2011-01-19 31 views
31

¿Cómo se imprime un doble en una secuencia para que cuando se lea no pierda precisión?Impresión doble sin perder precisión

me trataron:

std::stringstream ss; 

double v = 0.1 * 0.1; 
ss << std::setprecision(std::numeric_limits<T>::digits10) << v << " "; 

double u; 
ss >> u; 
std::cout << "precision " << ((u == v) ? "retained" : "lost") << std::endl; 

Esto no funcionó como esperaba.

Pero puedo aumentar la precisión (lo cual me sorprendió ya que pensé que digits10 era el máximo requerido).

ss << std::setprecision(std::numeric_limits<T>::digits10 + 2) << v << " "; 
               // ^^^^^^ +2 

Tiene que ver con el número de dígitos significativos y los primeros dos no cuentan (0.01).

¿Alguien ha considerado representar los números de punto flotante exactamente? ¿Cuál es el conjuro mágico exacto en la transmisión que debo hacer?

Después de algunos experimentos:

El problema era con mi versión original. Hubo dígitos no significativos en la cadena después del punto decimal que afectó la precisión.

Así que para compensar esto podemos usar la notación científica para compensar:

ss << std::scientific 
    << std::setprecision(std::numeric_limits<double>::digits10 + 1) 
    << v; 

Esto todavía no explica la necesidad de que el 1 aunque.

¡También si imprimo el número con más precisión obtengo más precisión impresa!

std::cout << std::scientific << std::setprecision(std::numeric_limits<double>::digits10) << v << "\n"; 
std::cout << std::scientific << std::setprecision(std::numeric_limits<double>::digits10 + 1) << v << "\n"; 
std::cout << std::scientific << std::setprecision(std::numeric_limits<double>::digits) << v << "\n"; 

el resultado es:

1.000000000000000e-02 
1.0000000000000002e-02 
1.00000000000000019428902930940239457413554200000000000e-02 

Basado en @Stephen Canon respuesta a continuación:

podemos imprimir exactamente utilizando el formateador printf(), "% a" o "% UN". Para lograr esto en C++ que tenemos que utilizar los manipuladores fijos y científicos (ver N3225: 22.4.2.2.2p5 Tabla 88)

std::cout.flags(std::ios_base::fixed | std::ios_base::scientific); 
std::cout << v; 

Por ahora he definido:

template<typename T> 
std::ostream& precise(std::ostream& stream) 
{ 
    std::cout.flags(std::ios_base::fixed | std::ios_base::scientific); 
    return stream; 
} 

std::ostream& preciselngd(std::ostream& stream){ return precise<long double>(stream);} 
std::ostream& precisedbl(std::ostream& stream) { return precise<double>(stream);} 
std::ostream& preciseflt(std::ostream& stream) { return precise<float>(stream);} 

siguiente: ¿Cómo manejamos NaN/Inf?

+0

¿Por qué está incluyendo un espacio después de 'v' al salir a' ss'? – chrisaycock

+0

@chrisaycock: No hay razón. Cortar y pegar el error. –

+1

Siempre habrá una pérdida de precisión, excepto aquellos valores que tienen un denomenador binario. La pregunta debería ser * ¿Cuánta precisión se debe mantener? * –

Respuesta

14

No imprimir los valores de punto flotante en decimal si no quiere perder precisión. Incluso si imprime suficientes dígitos para representar exactamente el número, no todas las implementaciones tienen conversiones redondeadas correctamente a/desde cadenas decimales en todo el rango de coma flotante, por lo que aún puede perder precisión.

Use coma flotante hexadecimal en su lugar. En C:

printf("%a\n", yourNumber); 

C++ 0x proporciona el manipulador hexfloat para iostreams que hace lo mismo (en algunas plataformas, utilizando el modificador std::hex tiene el mismo resultado, pero esto no es una suposición portátil). se prefiere

Uso de punto flotante hex por varias razones.

En primer lugar, el valor impreso es siempre exacto. No se produce redondeo al escribir o leer un valor formateado de esta manera.Más allá de los beneficios de precisión, esto significa que leer y escribir dichos valores puede ser más rápido con una biblioteca de E/S bien ajustada. También requieren menos dígitos para representar valores exactamente.

+0

¿Este especificador de tipo está presente en todos los tiempos de ejecución? Tengo en Visual C++, pero algunas referencias no lo tienen. http://www.cplusplus.com/reference/clibrary/cstdio/printf/ – ThomasMcLeod

+1

El especificador '% a' ha estado en el estándar C durante 11 años; cualquier plataforma que todavía no lo admite no puede afirmar que es "C". 'hexfloat' se agregó en C++ 0x (creo, no soy un chico de C++), por lo que su uso puede ser algo menos portátil. –

+0

Puede obtener el formateador% a especificando el formato fijo y científico. –

5

Una doble tiene la precisión de 52 dígitos binarios o 15.95 dígitos decimales. Ver http://en.wikipedia.org/wiki/IEEE_754-2008. Necesita al menos 16 dígitos decimales para registrar la precisión completa de un doble en todos los casos. [Pero vea cuarta edición, abajo].

Por cierto, esto significa dígitos significativos.

respuesta a OP edita:

Su punto flotante a tiempo de ejecución cadena decimal se outputing manera más dígitos de los que son significativos. Un doble solo puede contener 52 bits de significado (en realidad, 53, si cuenta un 1 "oculto" 1 que no está almacenado). Eso significa que la resolución no es más de 2^-53 = 1.11e-16.

Por ejemplo: 1 + 2^-52 = 1.0000000000000002220446049250313. . . .

Esos dígitos decimales, .0000000000000002220446049250313. . . . son el "paso" binario más pequeño en un doble cuando se convierte a decimal.

El "paso" dentro de la doble es:

.0000000000000000000000000000000000000000000000000001 en binario.

Tenga en cuenta que el paso binario es exacto, mientras que el paso decimal es inexacto.

Por lo tanto la representación decimal anteriormente,

1,0000000000000002220446049250313. . .

es una representación inexacta del número binario exacta:

1,0000000000000000000000000000000000000000000000000001.

Tercera Edición:

El siguiente valor posible para un doble, que en binario exacta es:

1,0000000000000000000000000000000000000000000000000010

convierte inexacta en decimal a

1,0000000000000004440892098500626. . . .

Así que todos esos dígitos adicionales en el decimal no son realmente significativos, solo son artefactos de conversión de base.

Cuarta Edición:

Aunque un doble en la mayoría de las tiendas de 16 dígitos decimales significativos, veces 17 dígitos decimales son necesarios para representar el número. La razón tiene que ver con rebanada de dígitos.

Como mencioné anteriormente, hay 52 + 1 dígitos binarios en el doble. El "+1" es una supuesta ventaja 1, y no se almacena ni es significativo. En el caso de un entero, esos 52 dígitos binarios forman un número entre 0 y 2^53 - 1. ¿Cuántos dígitos decimales son necesarios para almacenar dicho número? Bueno, log_10 (2^53 - 1) es aproximadamente 15.95. Así que a lo más 16 dígitos decimales son necesarios. Etiquetemos estos d_0 a d_15.

Ahora considere que los números de punto flotante IEEE también tienen un binario exponente. ¿Qué sucede cuando incrementamos el exponente por, digamos, 2? Hemos multiplicado nuestro número de 52 bits, sea lo que sea, por 4. Ahora, en lugar de nuestros 52 dígitos binarios alineados perfectamente con nuestros dígitos decimales d_0 a d_15, tenemos algunos dígitos binarios significativos representados en d_16. Sin embargo, dado que multiplicamos por algo menos de 10, todavía tenemos dígitos binarios significativos representados en d_0. Entonces nuestros dígitos decimales de 15.95 ahora ocupan d_1 a d_15, más algunos bits superiores de d_0 y algunos bits más bajos de d_16. Esta es la razón por la que a veces se necesitan 17 dígitos decimales para representar un doble IEEE.

Quinta Edición

errores numéricos fijos

+0

Cuando uso la notación científica y una precisión, esto funciona exactamente como usted lo describe. '(numeric_limits :: digits10 + 1) == 16'. Y en mi código original esto indica que no se perdió precisión. Pero cuando imprimo con 53 dígitos, indica que hay más precisión que la que estaba usando (consulte la edición anterior). No entiendo la discrepancia. –

16

¡No es correcto decir "punto flotante es inexacta", aunque reconozco que es una simplificación útil. Si usáramos la base 8 o 16 en la vida real, entonces la gente de aquí diría que "los paquetes de fracción decimal de base 10 son inexactos, ¿por qué alguien cocinó todo eso?".

El problema es que los valores enteros se traducen exactamente de una base a otra, pero los valores fraccionarios no lo hacen, porque representan fracciones del paso integral y sólo unos pocos de ellos se utilizan.

flotante aritmética de punto de vista técnico es perfectamente preciso. Cada cálculo tiene un único resultado posible. Hay es un problema, y ​​es que la mayoría de fracciones decimales tienen base 2 representaciones que se repiten. De hecho, en la secuencia 0.01, 0.02, ... 0.99, solo 3 meros valores tienen representaciones binarias exactas. (0.25, 0.50 y 0.75.) Hay 96 valores que se repiten y, por lo tanto, obviamente no están representados exactamente.

Ahora, hay una serie de formas de escribir y leer de nuevo los números de punto flotante sin perder un solo bit. La idea es evitar tratar de expresar el número binario con una fracción de base 10.

  • Escríbalas como binarias. Hoy en día, todo el mundo implementa el formato IEEE-754, por lo que siempre que elija un orden de bytes y escriba o lea solo ese orden de bytes, los números serán transferibles.
  • Escríbalos como valores enteros de 64 bits. Aquí puede usar la base habitual 10. (Porque representa el entero alias de 64 bits, no la fracción de 52 bits).

También puede escribir más dígitos de fracción decimal. Si esto es preciso bit a bit dependerá de la calidad de las bibliotecas de conversión y no estoy seguro de contar con la precisión perfecta (del software) aquí. Pero cualquier error será excesivamente pequeño y sus datos originales ciertamente no tienen información en los bits más bajos. (Ninguna de las constantes de la física y la química se conocen por 52 bits, ni se ha medido ninguna distancia en la Tierra con 52 bits de precisión.) Pero para una copia de seguridad o restauración donde la precisión bit a bit se puede comparar automáticamente, obviamente no es ideal

+0

Creo que quiso decir "relevante" para "corregir" :) – MSN

+2

@MSN: No, "correcto" es, bueno, correcto. El punto flotante es * a menudo * inexacto, pero también lo son muchos algoritmos enteros. Es completamente posible escribir algoritmos exactos en coma flotante (de hecho, esa es una gran parte de lo que me pagan por hacer). –

+0

@Stephen, bueno, también es irrelevante para la discusión. Creo que sería más "correcto" decir que el contexto define la precisión, no la representación. – MSN

3

La manera más fácil (para doble IEEE 754) de garantizar una conversión de ida y vuelta es usar siempre 17 dígitos significativos. Pero eso tiene la desventaja de incluir a veces dígitos de ruido innecesarios (0.1 → "0.10000000000000001").

Un enfoque que funcionó para mí es sprintf el número con 15 dígitos de precisión, luego verifique si atof le devuelve el valor original. Si no es así, prueba con 16 dígitos. Si que no funciona, utilice 17.

Es posible que desee probar David Gay's algorithm (utilizado en Python 3.1 para implementar float.__repr__).

+0

Gracias por esto. Esa es una muy buena idea. – JohnB

+2

Hay una anomalía interesante en el proceso "prueba 15, luego 16 y 17" que puede omitir una cadena de 16 dígitos que se activa de manera circular - ver mi artículo http://www.exploringbinary.com/the-shortest-decimal-string -that-round-trips-may-not-be-the-nearest/ –

1

Gracias a ThomasMcLeod para señalar el error en mi mesa de cómputo

Para garantizar la conversión de ida y vuelta usando 15 o 16 o 17 dígitos, sólo es posible durante un tiempo comparativamente pocos casos. El número 15.95 proviene de tomar 2^53 (1 bit implícito + 52 bits en la significante/"mantisa") que sale a un número entero en el rango 10^15 a 10^16 (más cerca de 10^16).

Considere un valor doble de precisión x con un exponente de 0, es decir, cae dentro del rango de rango de punto flotante 1.0 < = x < 2.0. El bit implícito marcará el componente 2^0 (parte) de x. El bit explícito más alto del significado indicará el siguiente exponente inferior (de 0) < => -1 => 2^-1 o el componente 0.5.

El siguiente bit 0,25, los siguientes después de 0,125, 0,0625, 0,03125, 0,015625 y así sucesivamente (ver tabla a continuación). El valor 1.5 se representará por lo tanto mediante dos componentes agregados juntos: el bit implícito que denota 1.0 y el bit de significado explícito más alto que denota 0.5.

Esto ilustra que desde el bit implícito hacia abajo tiene 52 bits adicionales y explícitos para representar posibles componentes donde el más pequeño es 0 (exponente) - 52 (bits explícitos en significando) = -52 => 2^-52 que de acuerdo a la tabla a continuación es ... bueno, pueden ver que tiene un poco más de 15.95 dígitos significativos (37 para ser exactos). Para decirlo de otra manera, el número más pequeño en el rango 2^0 que es! = 1.0 en sí es 2^0 + 2^-52 que es 1.0 + el número al lado de 2^-52 (abajo) = (exactamente) 1.0000000000000002220446049250313080847263336181640625, un valor que cuento como 53 dígitos significativos de largo. Con 17 dígitos de "precisión" de formateo, el número se mostrará como 1.0000000000000002 y esto dependerá de que la biblioteca se convierta correctamente.

Así que tal vez "conversión de ida y vuelta en 17 dígitos" no es realmente un concepto que sea válido (suficiente).

2^ -1 = 0.5000000000000000000000000000000000000000000000000000 
2^ -2 = 0.2500000000000000000000000000000000000000000000000000 
2^ -3 = 0.1250000000000000000000000000000000000000000000000000 
2^ -4 = 0.0625000000000000000000000000000000000000000000000000 
2^ -5 = 0.0312500000000000000000000000000000000000000000000000 
2^ -6 = 0.0156250000000000000000000000000000000000000000000000 
2^ -7 = 0.0078125000000000000000000000000000000000000000000000 
2^ -8 = 0.0039062500000000000000000000000000000000000000000000 
2^ -9 = 0.0019531250000000000000000000000000000000000000000000 
2^-10 = 0.0009765625000000000000000000000000000000000000000000 
2^-11 = 0.0004882812500000000000000000000000000000000000000000 
2^-12 = 0.0002441406250000000000000000000000000000000000000000 
2^-13 = 0.0001220703125000000000000000000000000000000000000000 
2^-14 = 0.0000610351562500000000000000000000000000000000000000 
2^-15 = 0.0000305175781250000000000000000000000000000000000000 
2^-16 = 0.0000152587890625000000000000000000000000000000000000 
2^-17 = 0.0000076293945312500000000000000000000000000000000000 
2^-18 = 0.0000038146972656250000000000000000000000000000000000 
2^-19 = 0.0000019073486328125000000000000000000000000000000000 
2^-20 = 0.0000009536743164062500000000000000000000000000000000 
2^-21 = 0.0000004768371582031250000000000000000000000000000000 
2^-22 = 0.0000002384185791015625000000000000000000000000000000 
2^-23 = 0.0000001192092895507812500000000000000000000000000000 
2^-24 = 0.0000000596046447753906250000000000000000000000000000 
2^-25 = 0.0000000298023223876953125000000000000000000000000000 
2^-26 = 0.0000000149011611938476562500000000000000000000000000 
2^-27 = 0.0000000074505805969238281250000000000000000000000000 
2^-28 = 0.0000000037252902984619140625000000000000000000000000 
2^-29 = 0.0000000018626451492309570312500000000000000000000000 
2^-30 = 0.0000000009313225746154785156250000000000000000000000 
2^-31 = 0.0000000004656612873077392578125000000000000000000000 
2^-32 = 0.0000000002328306436538696289062500000000000000000000 
2^-33 = 0.0000000001164153218269348144531250000000000000000000 
2^-34 = 0.0000000000582076609134674072265625000000000000000000 
2^-35 = 0.0000000000291038304567337036132812500000000000000000 
2^-36 = 0.0000000000145519152283668518066406250000000000000000 
2^-37 = 0.0000000000072759576141834259033203125000000000000000 
2^-38 = 0.0000000000036379788070917129516601562500000000000000 
2^-39 = 0.0000000000018189894035458564758300781250000000000000 
2^-40 = 0.0000000000009094947017729282379150390625000000000000 
2^-41 = 0.0000000000004547473508864641189575195312500000000000 
2^-42 = 0.0000000000002273736754432320594787597656250000000000 
2^-43 = 0.0000000000001136868377216160297393798828125000000000 
2^-44 = 0.0000000000000568434188608080148696899414062500000000 
2^-45 = 0.0000000000000284217094304040074348449707031250000000 
2^-46 = 0.0000000000000142108547152020037174224853515625000000 
2^-47 = 0.0000000000000071054273576010018587112426757812500000 
2^-48 = 0.0000000000000035527136788005009293556213378906250000 
2^-49 = 0.0000000000000017763568394002504646778106689453125000 
2^-50 = 0.0000000000000008881784197001252323389053344726562500 
2^-51 = 0.0000000000000004440892098500626161694526672363281250 
2^-52 = 0.0000000000000002220446049250313080847263336181640625 
+1

Primero, la conversión matemática no es correcta. Por ejemplo, 2^-7 es 0.0078125, no 0.0070125 como ha publicado.En segundo lugar, incluso si los dígitos en la última línea eran correctos, ** no son significativos. ** Son artefactos de conversión base. Ver mi publicación anterior. – ThomasMcLeod

+0

@ThomasMcLeod: gracias por señalar los errores. En cuanto a su declaración "no son significativos", me permito diferir. En una abrumadora mayoría de los casos, no serán significativos, pero en unos pocos lo harán. Mi publicación intentó señalar las complejidades del redondeo y la conversión al mostrar la cantidad de dígitos realmente involucrados. –

+0

@Olof, ¿cómo se define significante? Si dividimos 1 por 3, tenemos 0.3333333333333333 ..., pero eso no significa que tengamos dígitos significativos infinitos. Regla básica: el resultado de una operación matemática nunca puede tener dígitos más significativos que el número de dígitos significativos de cualquier entrada numérica a esa operación. – ThomasMcLeod

0

@ThomasMcLeod: Creo que la regla dígito significativo viene de mi campo, la física y significa algo más sutil:

Si usted tiene una medición que se obtiene el valor de 1,52 y no se puede leer más detalle la escala y diga que se supone que debe agregar otro número (por ejemplo, de otra medición porque la escala de esta era demasiado pequeña), digamos 2, entonces el resultado (obviamente) tiene solo dos decimales, es decir, 3.52. Pero del mismo modo, si agrega 1.1111111111 al valor 1.52, obtendrá el valor 2.63 (¡y nada más!).

El motivo de la regla es evitar que se engañe al pensar que obtuvo más información de un cálculo que la medida (lo cual es imposible, pero parecería así llenándolo con basura, vea encima).

Dicho esto, esta regla específica es sólo para sumar (para la suma: el error del resultado es la suma de los dos errores, por lo que si mides solo uno mal, aunque suene, ahí va tu precisión ...) .

Cómo obtener las otras reglas: Digamos que a es el número medido y δa el error. Digamos que tu fórmula original fue: f: = m a Digamos que también mides m con el error δm (deja que sea el lado positivo). Entonces el límite real es: f_up = (m +? M) (a? A +) y f_down = (m-? M) (a-? A) Así, f_up = m a +? M? A + (a? M + m δa) f_down = m a + δm δa- (δm a + m δa) Por lo tanto, ahora los dígitos significativos son aún menores: f_up ~ m a + (δm a + m δa) f_down ~ m a- (δm a + m? a) y así ? F =? m a + m? a Si nos fijamos en el error relativo, que se obtiene: ? F/f =? m/m +? a/a

Para la división es Df/f = δm/m-δa/a

esperanza de que obtiene a través de la esencia y la esperanza no hice demasiados errores, es tarde aquí :-)

tl, dr: Los dígitos significativos decir cuántos de los dígitos en la salida realidad provienen de los dígitos en su entrada (en el mundo real, no en la imagen distorsionada que tienen los números en coma flotante). Si sus medidas fueron 1 con error "no" y 3 con error "no" y se supone que la función es 1/3, entonces sí, todos los dígitos infinitos son dígitos significativos reales. De lo contrario, la operación inversa no funcionaría, así que obviamente tienen que serlo.

Si la regla significativa cifra significa algo completamente diferente en otro campo, continúe :-)

9

Me interesé en esta pregunta porque estoy tratando (de) serializar mis datos a & de JSON.

creo que tengo una explicación más clara (con menos mano de renuncia) de por qué 17 dígitos decimales son suficientes para reconstruir el número original sin pérdidas:

enter image description here

Imagínese 3 líneas de números: 1.
de la base original 2 número
2. para la base redondeada 10 representación
3. para el número reconstruida (igual que # 1 porque tanto en la base 2)

Al convertir a la base 10, gráficamente, elige el tic en la línea del segundo número más cercano al tic en la 1ra. Del mismo modo cuando reconstruye el original del valor base 10 redondeado.

La observación crítica que tuve fue que, para permitir una reconstrucción exacta, el tamaño de la base de 10 pasos (cuántica) tiene que ser < la base 2 cuántica. De lo contrario, inevitablemente obtendrá la mala reconstrucción que se muestra en rojo.

Tome el caso específico de cuando el exponente es 0 para la representación de base2. Entonces el quantum base2 será 2^-52 ~ = 2.22 * 10^-16. El quantum 10 más cercano que es menor que esto es 10^-16. Ahora que conocemos el quantum requerido de la base 10, ¿cuántos dígitos se necesitarán para codificar todos los valores posibles? Dado que solo estamos considerando el caso de exponente = 0, el rango dinámico de valores que necesitamos representar es [1.0, 2.0). Por lo tanto, se necesitarían 17 dígitos (16 dígitos para la fracción y 1 dígito para la parte entera).

Para exponentes distinto de 0, podemos utilizar la misma lógica:

 
    exponent base2 quant. base10 quant. dynamic range digits needed 
    --------------------------------------------------------------------- 
    1    2^-51   10^-16   [2, 4)   17 
    2    2^-50   10^-16   [4, 8)   17 
    3    2^-49   10^-15   [8, 16)   17 
    ... 
    32    2^-20   10^-7  [2^32, 2^33)  17 
    1022   9.98e291  1.0e291 [4.49e307,8.99e307) 17 

Aunque no es exhaustiva, la tabla muestra la tendencia que 17 dígitos son suficientes.

Espero les guste mi explicación.

+1

http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html –

+0

Gracias por los votos al alza. Obtuve más información. El quantum de la base 10 tiene que ser <= el quantum de la base 2 porque esa es la única forma en que garantiza que para cada punto en la recta numérica de la base 2, el tic de la base 10 más cercano está dentro de 1/2 a un paso. Eso asegura una conversión exacta. –

Cuestiones relacionadas