c# c++ loops floating-point counter

c# - Usar "doble" como variables de contador en bucles



c++ loops (6)

En un libro que estoy leyendo actualmente, hay este extracto:

También puede usar un valor de coma flotante como contador de bucle. Aquí hay un ejemplo de un bucle for con este tipo de contador:

double a(0.3), b(2.5); for(double x = 0.0; x <= 2.0; x += 0.25) cout << "/n/tx = " << x << "/ta*x + b = " << a*x + b;

Este fragmento de código calcula el valor de a*x+b para valores de x de 0.0 a 2.0 , en pasos de 0.25 ; sin embargo, debe tener cuidado al usar un contador de coma flotante en un bucle. Muchos valores decimales no se pueden representar exactamente en forma de coma flotante binaria, por lo que las discrepancias pueden acumularse con valores acumulativos. Esto significa que no debe codificar un bucle for para que el final del bucle dependa de un contador de bucle flotante que alcance un valor preciso. Por ejemplo, el siguiente bucle mal diseñado nunca termina:

for(double x = 0.0 ; x != 1.0 ; x += 0.2) cout << x;

La intención de este ciclo es generar el valor de x ya que varía de 0.0 a 1.0 ; sin embargo, 0.2 no tiene representación exacta como un valor de punto flotante binario, por lo que el valor de x nunca es exactamente 1 . Por lo tanto, la segunda expresión de control de bucle siempre es falsa, y el bucle continúa indefinidamente.

¿Puede alguien explicar cómo se ejecuta el primer bloque de código mientras que el segundo no?


El primer bloque usa una condición de menor o igual que ( <= ).

Incluso con imprecisiones de coma flotante, eso eventualmente será falso.


El primero eventualmente terminará, incluso si x no alcanza exactamente 2.0 ... porque terminará siendo mayor que 2.0, y así se romperá.

El segundo debería hacer que x golpee exactamente 1.0 para romperse.

Es desafortunado que el primer ejemplo use un paso de 0.25, que es exactamente representable en coma flotante binario; hubiera sido más inteligente hacer que ambos ejemplos usen 0.2 como el tamaño del paso. (0.2 no es exactamente representable en punto flotante binario).


Este es un ejemplo de un problema más amplio: cuando se comparan los dobles, a menudo se necesita verificar la igualdad dentro de una tolerancia aceptable en lugar de la igualdad exacta.

En algunos casos, por lo general, la comprobación de un valor predeterminado sin cambios, la igualdad está bien:

double x(0.0); // do some work that may or may not set up x if (x != 0.0) { // do more work }

En general, sin embargo, la verificación en comparación con un valor esperado no se puede hacer de esa manera; necesitaría algo como:

double x(0.0); double target(10000.0); double tolerance(0.000001); // do some work that may or may not set up x to an expected value if (fabs(target - x) < tolerance) { // do more work }


Hay varios problemas con el ejemplo, y dos cosas son diferentes entre los casos.

  • Una comparación que involucra la igualdad de punto flotante requiere conocimiento experto del dominio, por lo que es más seguro usar < o > para los controles de bucle.

  • El incremento de bucle 0.25 realmente tiene una representación exacta

  • El incremento de bucle 0.2 no tiene una representación exacta

  • En consecuencia, es posible verificar exactamente la suma de muchos incrementos de 0,25 (o 1,0 ), pero no es posible hacer coincidir exactamente ni siquiera un incremento de 0,2 .

A menudo se cita una regla general: no haga comparaciones de igualdad de números de coma flotante. Si bien este es un buen consejo general, cuando se trata de enteros o enteros más fracciones compuestas de ½ + ¼ ... puede esperar representaciones exactas.

Y preguntaste por qué ? La respuesta corta es: debido a que las fracciones se representan como ½ + ¼ ..., la mayoría de los números decimales no tienen representaciones exactas ya que no se pueden factorizar en potencias de dos. Esto significa que las representaciones internas de FP son largas cadenas de bits que se redondearán a un valor esperado para la salida, pero que en realidad no son exactamente ese valor.


La práctica general es que no se comparan dos números de coma flotante, es decir:

// using System.Diagnostics; double a = 0.2; a *= 5.0; double b = 1.0; Debug.Assert(a == b);

Debido a la imprecisión de los números de coma flotante, a podría no ser exactamente igual a b . Para comparar por igualdad, puede comparar la diferencia de dos números con un valor de tolerancia:

Debug.Assert(Math.Abs(a - b) < 0.0001);


Los números de coma flotante se representan internamente como un número binario, casi siempre en formato IEEE. Puede ver cómo se representan los números aquí:

http://babbage.cs.qc.edu/IEEE-754/

Por ejemplo, 0.25 en binario es 0.01 b y se representa como +1.00000000000000000000000 * 2 -2 .

Esto se almacena internamente con 1 bit para el signo, ocho bits para el exponente (que representa un valor entre -127 y +128, y 23 bits para el valor (el 1. principal no se almacena). De hecho, los bits son:

[0] [01111101] [00000000000000000000000]

Mientras que 0.2 en binario no tiene representación exacta, al igual que 1/3 no tiene representación exacta en decimal.

Aquí el problema es que al igual que 1/2 puede representarse exactamente en formato decimal como 0.5, pero 1/3 solo puede aproximarse a 0.3333333333, 0.25 puede representarse exactamente como una fracción binaria, pero 0.2 no puede representarse. En binario es 0.0010011001100110011001100 .... b donde se repiten los últimos cuatro dígitos.

Para almacenar en una computadora, se rutea a 0.0010011001100110011001101 b . Lo cual es realmente, muy cerca, así que si estás calculando coordenadas o cualquier otra cosa donde importen los valores absolutos, está bien.

Desafortunadamente, si agrega ese valor a sí mismo cinco veces, obtendrá 1.00000000000000000000001 b . (O, si hubiera redondeado 0.2 a 0.0010011001100110011001100 b en su lugar, obtendría 0.11111111111111111111100 b )

De cualquier manera, si su condición de bucle es 1.00000000000000000000001 b == 1.00000000000000000000000 b , no terminará. Si usa <= en su lugar, es posible que se ejecute un tiempo adicional si el valor está justo por debajo del último valor, pero se detendrá.

Sería posible crear un formato que pueda representar con precisión pequeños valores decimales (como cualquier valor con solo dos lugares decimales). Se usan en cálculos financieros, etc. Pero los valores normales de coma flotante funcionan así: intercambian la capacidad de representar algunos pequeños números "fáciles" como 0.2 para la capacidad de representar un amplio rango de forma consistente.

Es común evitar el uso de un flotador como contador de bucle por esa razón exacta, las soluciones comunes serían:

  • Si una iteración extra no importa, use <=
  • Si es importante, establezca la condición <= 1,0001 en su lugar, o algún otro valor más pequeño que su incremento, por lo que los errores off-by-0.0000000000000000000001 no importan
  • Usa un número entero y divídelo por algo durante el ciclo
  • Use una clase especialmente hecha para representar valores fraccionarios exactamente

Sería posible para un compilador optimizar un ciclo flotante "=" para convertirlo en lo que quiere decir que sucederá, pero no sé si eso está permitido por el estándar o si alguna vez sucede en la práctica.