remarks example cref c# .net math numeric

example - params comments c#



Matemática de asociatividad:(a+b)+c!=A+(b+c) (6)

Recientemente estaba revisando una vieja publicación de blog de Eric Lippert en la que, al escribir sobre asociatividad, menciona que en C #, (a + b) + c no es equivalente a a + (b + c) para ciertos valores de a, b c.

No puedo averiguar qué tipos y rango de valores aritméticos podrían ser ciertos y por qué.


Ampliando las otras respuestas que muestran cómo con extremos de números pequeños y grandes se obtiene un resultado diferente, aquí hay un ejemplo donde el punto flotante con números normales realistas le da una respuesta diferente.

En este caso, en lugar de usar números en los límites extremos de precisión, simplemente hago muchas adiciones. La diferencia es hacer (((...(((a+b)+c)+d)+e)... o ...(((a+b)+(c+d))+((e+f)+(g+h)))+...

Estoy usando Python aquí, pero probablemente obtendrá los mismos resultados si escribe esto en C #. Primero cree una lista de un millón de valores, todos los cuales son 0.1. Súmelos desde la izquierda y verá que los errores de redondeo se vuelven significativos:

>>> numbers = [0.1]*1000000 >>> sum(numbers) 100000.00000133288

Ahora agréguelos nuevamente, pero esta vez agréguelos en pares (hay formas mucho más eficientes de hacer esto que usan menos almacenamiento intermedio, pero mantuve la implementación simple aquí):

>>> def pair_sum(numbers): if len(numbers)==1: return numbers[0] if len(numbers)%2: numbers.append(0) return pair_sum([a+b for a,b in zip(numbers[::2], numbers[1::2])]) >>> pair_sum(numbers) 100000.0

Esta vez, se minimizan los errores de redondeo.

Edite para completar, aquí hay una implementación más eficiente pero menos fácil de seguir de una suma por pares. Da la misma respuesta que el pair_sum() anterior:

def pair_sum(seq): tmp = [] for i,v in enumerate(seq): if i&1: tmp[-1] = tmp[-1] + v i = i + 1 n = i & -i while n > 2: t = tmp.pop(-1) tmp[-1] = tmp[-1] + t n >>= 1 else: tmp.append(v) while len(tmp) > 1: t = tmp.pop(-1) tmp[-1] = tmp[-1] + t return tmp[0]

Y aquí está el simple par_sum escrito en C #:

using System; using System.Linq; namespace ConsoleApplication1 { class Program { static double pair_sum(double[] numbers) { if (numbers.Length==1) { return numbers[0]; } var new_numbers = new double[(numbers.Length + 1) / 2]; for (var i = 0; i < numbers.Length - 1; i += 2) { new_numbers[i / 2] = numbers[i] + numbers[i + 1]; } if (numbers.Length%2 != 0) { new_numbers[new_numbers.Length - 1] = numbers[numbers.Length-1]; } return pair_sum(new_numbers); } static void Main(string[] args) { var numbers = new double[1000000]; for (var i = 0; i < numbers.Length; i++) numbers[i] = 0.1; Console.WriteLine(numbers.Sum()); Console.WriteLine(pair_sum(numbers)); } } }

con salida:

100000.000001333 100000


En el rango del tipo double :

double dbl1 = (double.MinValue + double.MaxValue) + double.MaxValue; double dbl2 = double.MinValue + (double.MaxValue + double.MaxValue);

El primero es double.MaxValue , el segundo es double.Infinity

Sobre la precisión del tipo double :

double dbl1 = (double.MinValue + double.MaxValue) + double.Epsilon; double dbl2 = double.MinValue + (double.MaxValue + double.Epsilon);

Ahora dbl1 == double.Epsilon , mientras que dbl2 == 0 .

Y al leer literalmente la pregunta :-)

En modo checked :

checked { int i1 = (int.MinValue + int.MaxValue) + int.MaxValue; }

i1 es int.MaxValue

checked { int temp = int.MaxValue; int i2 = int.MinValue + (temp + temp); }

(tenga en cuenta el uso de la variable temp , de lo contrario el compilador dará un error directamente ... Técnicamente, incluso este sería un resultado diferente :-) Compila correctamente vs no compila)

esto arroja una Exception OverflowException ... Los resultados son diferentes :-) ( int.MaxValue vs Exception )


Esto se debe al hecho de que los tipos de valores ordinarios (int, long, etc.) se almacenan utilizando una cantidad fija de bytes. Por lo tanto, el desbordamiento es posible cuando la suma de dos valores excede la capacidad de almacenamiento de bytes.

En C #, uno puede usar BigInteger para evitar este tipo de problema. Los BigInteger tienen un tamaño arbitrario y, por lo tanto, no crean desbordamientos.

BigInteger solo está disponible desde .NET 4.0 y superior (VS 2010+).


La respuesta corta es (a + b) + c == a + (b + c) matemáticamente, pero no necesariamente computacionalmente.

Recordando que las computadoras realmente funcionan en binario, incluso los decimales simples pueden dar lugar a errores de redondeo cuando se convierten a formato interno.

Dependiendo del idioma, incluso la suma puede incurrir en errores de redondeo, y en el ejemplo anterior, el error de redondeo en a+b puede diferir del de b+c .

Un delincuente sorprendente es JavaScript: 0.1 + 0.2 != 0.3 . El error de redondeo está muy por debajo del decimal, pero es real y problemático.

Es un principio general que reduzca el error de redondeo agregando primero las partes pequeñas. De esta manera, pueden acumularse antes de ser abrumados por el mayor número.


Un par de ejemplos similares:

static void A(string s, int i, int j) { var test1 = (s + i) + j; var test2 = s + (i + j); var testX = s + i + j; }

Aquí A("Hello", 3, 5) conduce a que test1 y testX sean iguales a "Hello35" , mientras que test2 será "Hello8" .

Y:

static void B(int i, int j, long k) { var test1 = (i + j) + k; var test2 = i + (j + k); var testX = i + j + k; }

Aquí B(2000000000, 2000000000, 42L) conduce a test1 y testX es igual a -294967254L en el modo habitual unchecked -294967254L , mientras que test2 convierte en 4000000042L .


un ejemplo

a = 1e-30 b = 1e+30 c = -1e+30