c++ - resueltos - que es coma flotante en informatica
¿Cómo la computadora hace la aritmética de punto flotante? (5)
El problema es que el formato de punto flotante representa fracciones en la base 2.
El primer bit de fracción es ½, el segundo ¼, y continúa como 1/2 n .
Y el problema con eso es que no todos los números racionales (un número que se puede expresar como la proporción de dos enteros) en realidad tienen una representación finita en este formato de base 2.
(Esto hace que el formato de coma flotante sea difícil de usar para los valores monetarios. Aunque estos valores son siempre números racionales ( n / 100), solo .00, .25, .50 y .75 en realidad tienen representaciones exactas en cualquier número de dígitos de un base dos fracciones)
De todos modos, cuando los agrega de nuevo, el sistema finalmente tiene la oportunidad de redondear el resultado a un número que puede representar exactamente.
En algún momento, se encuentra agregando el número .666 ... al .333 ... uno, así:
00111110 1 .o10101010 10101010 10101011
+ 00111111 0 .10101010 10101010 10101011o
------------------------------------------
00111111 1 (1).0000000 00000000 0000000x # the x isn''t in the final result
El bit más a la izquierda es el signo, los siguientes ocho son el exponente, y los bits restantes son la fracción. Entre el exponente y la fracción se encuentra un supuesto "1" que siempre está presente y, por lo tanto, no está realmente almacenado, como el bit de fracción más a la izquierda normalizado. He escrito ceros que en realidad no están presentes en bits individuales como o
.
Mucho ha sucedido aquí, en cada paso, la FPU ha tomado medidas bastante heroicas para redondear el resultado. Se han conservado dos dígitos adicionales de precisión (más allá de lo que cabe en el resultado), y la FPU sabe en muchos casos si alguno, o al menos 1, de los bits más a la derecha restantes fueron uno. Si es así, entonces esa parte de la fracción es más de 0.5 (escalada) y entonces se redondea hacia arriba. Los valores redondeados intermedios permiten a la FPU llevar el bit del extremo derecho a la parte entera y finalmente redondear a la respuesta correcta.
Esto no sucedió porque alguien agregó 0.5; la FPU simplemente hizo lo mejor que pudo dentro de las limitaciones del formato. El punto flotante no es, en realidad, inexacto. Es perfectamente exacto, pero la mayoría de los números que esperamos ver en nuestra vista mundial de base-10, número racional, no son representables por la fracción de base-2 del formato. De hecho, muy pocos lo son.
He visto artículos extensos que explican cómo se pueden almacenar números flotantes y cómo se está haciendo la aritmética de esos números, pero por favor explique brevemente por qué cuando escribo
cout << 1.0 / 3.0 <<endl;
Veo 0.333333 , pero cuando escribo
cout << 1.0 / 3.0 + 1.0 / 3.0 + 1.0 / 3.0 << endl;
Yo veo 1 .
¿Cómo hace la computadora esto? Por favor explica solo este simple ejemplo. Es suficiente para mí.
En cuanto a este ejemplo específico: creo que los compiladores son demasiado inteligentes hoy en día, y automáticamente se aseguran de que un resultado const
de tipos primitivos sea exacto si es posible. No he logrado engañar a g ++ para que haga un cálculo fácil como este.
Sin embargo, es fácil pasar por alto tales cosas mediante el uso de variables no const. Todavía,
int d = 3;
float a = 1./d;
std::cout << d*a;
rendirá exactamente 1, aunque esto no debería ser esperado. La razón, como ya se dijo, es que el operator<<
redondea el error.
En cuanto a por qué puede hacer esto: cuando agrega números de un tamaño similar o multiplica un float
por un int
, obtiene casi toda la precisión que el tipo de flotación puede ofrecerle al máximo, lo que significa que el error / resultado de la relación es muy pequeño ( en otras palabras, los errores ocurren en una posición decimal tardía, suponiendo que tiene un error positivo).
Así que 3*(1./3)
, aunque, como un flotador, no exactamente ==1
, tiene un gran sesgo correcto que evita que el operator<<
se encargue de los pequeños errores. Sin embargo, si luego quita este sesgo simplemente restando 1, el punto flotante se deslizará hacia la derecha hasta el error, y de repente ya no se puede olvidar. Como dije, esto no ocurre si solo 3*(1./3)-1
porque el compilador es demasiado inteligente, pero intenta
int d = 3;
float a = 1./d;
std::cout << d*a << " - 1 = " << d*a - 1 << " ???/n";
Lo que obtengo (g ++, 32 bit Linux) es
1 - 1 = 2.98023e-08 ???
Esto funciona porque la precisión predeterminada es de 6 dígitos, y redondeada a 6 dígitos, el resultado es 1. Ver 27.5.4.1 constructores de basic_ios en el borrador del estándar C ++ (n3092) .
Hagamos las matemáticas. Para abreviar, suponemos que solo tiene cuatro dígitos significativos (base-2).
Por supuesto, dado que gcd(2,3)=1
, 1/3
es periódico cuando se representa en base-2. En particular, no se puede representar exactamente, así que tenemos que contentarnos con la aproximación
A := 1×1/4 + 0×1/8 + 1×1/16 + 1*1/32
que está más cerca del valor real de 1/3
que
A'' := 1×1/4 + 0×1/8 + 1×1/16 + 0×1/32
Por lo tanto, imprimir A
en decimal da 0.34375 (el hecho de que vea 0.33333 en su ejemplo es solo un testamento del mayor número de dígitos significativos en un double
).
Al sumar estas tres veces, obtenemos
A + A + A
= ( A + A ) + A
= ( (1/4 + 1/16 + 1/32) + (1/4 + 1/16 + 1/32) ) + (1/4 + 1/16 + 1/32)
= ( 1/4 + 1/4 + 1/16 + 1/16 + 1/32 + 1/32 ) + (1/4 + 1/16 + 1/32)
= ( 1/2 + 1/8 + 1/16 ) + (1/4 + 1/16 + 1/32)
= 1/2 + 1/4 + 1/8 + 1/16 + 1/16 + O(1/32)
El término O(1/32)
no se puede representar en el resultado, por lo que se descarta y obtenemos
A + A + A = 1/2 + 1/4 + 1/8 + 1/16 + 1/16 = 1
QED :)
Vea el artículo sobre "Lo que cada científico informático debe saber sobre la aritmética de punto flotante"