c - por - integrales indefinidas
imprimiendo la parte integral de un nĂºmero de coma flotante (6)
Creo que el problema radica en value / = base; No olvide que 10 no es una fracción finita en el sistema binario y, por lo tanto, este cálculo nunca es correcto. También supongo que se producirá algún error en fmod debido a la misma razón.
printf primero calculará la parte integral y luego la convertirá a decimal (si obtengo la forma en que imprime la parte integral correctamente).
Estoy tratando de averiguar cómo imprimir números de coma flotante sin usar las funciones de la biblioteca. Imprimir la parte decimal de un número de punto flotante resultó ser bastante fácil. Imprimir la parte integral es más difícil:
static const int base = 2;
static const char hex[] = "0123456789abcdef";
void print_integral_part(float value)
{
assert(value >= 0);
char a[129]; // worst case is 128 digits for base 2 plus NUL
char * p = a + 128;
*p = 0;
do
{
int digit = fmod(value, base);
value /= base;
assert(p > a);
*--p = hex[digit];
} while (value >= 1);
printf("%s", p);
}
La impresión de la parte integral de FLT_MAX
funciona sin problemas con la base 2 y la base 16:
11111111111111111111111100000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000 (base 2)
ffffff00000000000000000000000000 (base 16)
Sin embargo, la impresión en la base 10 da como resultado errores después de los primeros 7 dígitos:
340282368002860660002286082464244022240 (my own function)
340282346638528859811704183484516925440 (printf)
Supongo que esto es un resultado de la división por 10. Mejora si uso double en lugar de float:
340282346638528986604286022844204804240 (my own function)
340282346638528859811704183484516925440 (printf)
(Si no cree en printf
, ingrese 2^128-2^104
en Wolfram Alpha. Es correcto).
Ahora, ¿cómo printf
logra imprimir el resultado correcto? ¿Utiliza algunas instalaciones de bigint internamente? ¿O hay algún truco de punto flotante que me falta?
De acuerdo con la implementación de flotación de precisión simple de IEEE, solo se almacenan 24 bits de datos en cualquier momento en una variable flotante. Esto significa que solo se almacenan un máximo de 7 dígitos decimales en el número flotante.
El resto de la enormidad del número se almacena en el exponente. FLT_MAX se inicializa como 3.402823466e + 38F. Entonces, después de la décima precisión, qué dígito debería imprimirse no está definido en ninguna parte.
Desde el compilador de Visual C ++ 2010, obtengo esta salida 340282346638528860000000000000000000000.000000, que es la única salida de vaild.
Entonces, inicialmente tenemos estos muchos dígitos válidos 3402823466 Entonces, después de la 1.ª división, tenemos solo 0402823466 Por lo tanto, el sistema necesita deshacerse del 0 izquierdo e introducir un nuevo dígito a la derecha. En la división entera ideal, es 0. Como está haciendo una división flotante (valor / = base;), el sistema está obteniendo algún otro dígito para completar esa ubicación.
Entonces, en mi opinión, printf podría estar asignando los dígitos significativos disponibles arriba a un número entero y trabajando con esto.
Vamos a explicar esto una vez más. Después de que la parte entera se haya impreso (exactamente) sin ningún redondeo que no sea chop hacia 0, es hora de los bits decimales.
Comience con una cadena de bytes (digamos 100 para principiantes) que contenga ceros binarios. Si se establece el primer bit a la derecha del punto decimal en el valor de fp, significa que 0.5 (2 ^ -1 o 1 / (2 ^ 1) es un componente de la fracción. Por lo tanto, agregue 5 al primer byte. el siguiente bit se establece en 0.25 (2 ^ -2 o 1 / (2 ^ 2)) es parte de la fracción agregue 5 al segundo byte y agregue 2 al primero (oh, no olviden el carry, suceden - matemáticas de la escuela inferior). El siguiente conjunto de bits significa 0.125 así que agrega 5 al tercer byte, 2 al segundo y 1 al primero. Y así sucesivamente:
value string of binary 0s
start 0 0000000000000000000 ...
bit 1 0.5 5000000000000000000 ...
bit 2 0.25 7500000000000000000 ...
bit 3 0.125 8750000000000000000 ...
bit 4 0.0625 9375000000000000000 ...
bit 5 0.03125 9687500000000000000 ...
bit 6 0.015625 9843750000000000000 ...
bit 7 0.0078125 9921875000000000000 ...
bit 8 0.00390625 9960937500000000000 ...
bit 9 0.001953125 9980468750000000000 ...
...
Hice esto a mano, así que me pude haber perdido algo, pero implementarlo en el código es trivial.
Entonces, para todos aquellos que "no pueden obtener un resultado exacto usando float", las personas que no saben de lo que están hablando aquí son una prueba de que los valores de fracción de coma flotante son perfectamente exactos. Exactamente exactos. Pero binario.
Para aquellos que se toman el tiempo de entender cómo funciona esto, una mejor precisión está al alcance. En cuanto a los demás ... bueno, supongo que seguirán sin explorar los foros para la respuesta a una pregunta que ha sido respondida muchas veces anteriormente, honestamente creen que han descubierto un "punto flotante quebrado" (o lo que sea que lo llamen) y publica una nueva variante de la misma pregunta todos los días.
"Cerca de la magia", "conjuro oscuro": ¡eso es gracioso!
Parece que el caballo de trabajo para la conversión de float a string es la función dtoa()
. Ver dtoa.c en newlib para saber cómo lo hacen.
Ahora, ¿cómo printf logra imprimir el resultado correcto?
Creo que está cerca de la magia. Al menos la fuente parece una especie de conjuro oscuro.
¿Utiliza algunas instalaciones de bigint internamente?
Sí, busque _Bigint
en el archivo fuente vinculado.
¿O hay algún truco de punto flotante que me falta?
Probable.
/ Editar: Lee primero la respuesta de Unni . Estos resultados provienen de http://codepad.org/TLqQzLO3 .
void print_integral_part(float value)
{
printf("input : %f/n", value);
char a[129]; // worst case is 128 digits for base 2 plus NUL
char * p = a + 128;
*p = 0;
do
{
int digit = fmod(value, base);
value /= base;
printf("interm: %f/n", value);
*--p = hex[digit];
} while (value >= 1);
printf("result: %s/n", p);
}
print_integral_part(3.40282347e+38F);
para ver cuán desordenado se pone tu valor por la operación de value /= base
:
input : 340282346638528859811704183484516925440.000000
interm: 34028234663852885981170418348451692544.000000
interm: 3402823466385288480057879763104038912.000000
interm: 340282359315034876851393457419190272.000000
interm: 34028234346940236846450271659753472.000000
interm: 3402823335658820218996583884128256.000000
interm: 340282327376181848531187106054144.000000
interm: 34028232737618183051678859657216.000000
interm: 3402823225404785588136713388032.000000
interm: 340282334629736780292710989824.000000
interm: 34028231951816403862828351488.000000
interm: 3402823242405304929106264064.000000
interm: 340282336046446683592065024.000000
interm: 34028232866774907300610048.000000
interm: 3402823378911210969759744.000000
interm: 340282332126513595416576.000000
interm: 34028233212651357863936.000000
interm: 3402823276229139890176.000000
interm: 340282333252413489152.000000
interm: 34028234732616232960.000000
interm: 3402823561222553600.000000
interm: 340282356122255360.000000
interm: 34028235612225536.000000
interm: 3402823561222553.500000
interm: 340282366859673.625000
interm: 34028237357056.000000
interm: 3402823735705.600098
interm: 340282363084.799988
interm: 34028237619.200001
interm: 3402823680.000000
interm: 340282368.000000
interm: 34028236.800000
interm: 3402823.600000
interm: 340282.350000
interm: 34028.234375
interm: 3402.823438
interm: 340.282349
interm: 34.028235
interm: 3.402824
interm: 0.340282
result: 340282368002860660002286082464244022240
En caso de duda, arroje más printfs al respecto;)
Al igual que la respuesta de Agent_L, estás sufriendo el falso resultado causado al dividir el valor entre 10. Flotante, como cualquier tipo de punto flotante binario, no puede expresar correctamente el número más racional en decimales. Después de la división, la mayoría del caso, el resultado no se puede ajustar en binario, por lo que se redondeará. Por lo tanto, cuanto más divides, más errores te darás cuenta.
Si el número no es muy grande, una solución rápida sería multiplicarlo por 10 o una potencia de 10 dependiendo de cuántos dígitos después del punto decimal necesite.
Otra forma fue descrita aquí