Impresión doble sin perder precisión


¿Cómo se imprime un doble en una secuencia para que cuando se lee no se pierda precisión?

Lo intenté:

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 que 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 dos primeros no cuentan (0.01).

Entonces, ¿alguien ha visto representar números de coma flotante exactamente? ¿Cuál es el exacto encantamiento mágico en la corriente que tengo que hacer?

Después de algunos experimentos:

El problema estaba con mi versión original. Había dígitos no significativos en la cadena después del punto decimal que afectaban 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 del +1.

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

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";

Resulta en:

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

Basado en @Stephen Canon respuesta a continuación:

Podemos imprimir exactamente usando el formateador printf (), "%a" o "%A". Para lograr esto en C++ necesitamos usar 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?

Author: Peter Mortensen, 2011-01-19

7 answers

No imprima valores de coma flotante en decimal si no quiere perder precisión. Incluso si imprime suficientes dígitos para representar el número exactamente, 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.

Utilice el punto 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, usando el std::hex modificador tiene el mismo resultado, pero esto no es una suposición portátil).

Se prefiere usar coma flotante hexadecimal por varias razones.

Primero, el valor impreso siempre es 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 la lectura y escritura de tales valores puede ser más rápida con una biblioteca de E/S bien sintonizada. También requieren menos dígitos para representar valores exactamente.

 14
Author: Stephen Canon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-18 13:46:08

No es correcto decir "el punto flotante es inexacto", aunque admito que es una simplificación útil. Si usamos base 8 o 16 en la vida real, entonces la gente de por aquí estaría diciendo "los paquetes de fracción decimal de base 10 son inexactos, ¿por qué alguien los cocinó?".

El problema es que los valores integrales se traducen exactamente de una base a otra, pero los valores fraccionarios no lo hacen, porque representan fracciones del paso integral y solo unos pocos de ellos son utilizar.

La aritmética de coma flotante es técnicamente perfectamente precisa. Cada cálculo tiene un solo resultado posible. esun problema, y es que la mayoría de las fracciones decimales tienen representaciones base-2 que se repiten. De hecho, en la secuencia 0.01, 0.02,... 0.99, solo 3 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 un número de formas de escribir y leer números de coma flotante sin perder un solo bit. La idea es evitar tratar de expresar el número binario con una fracción base 10.

  • Escríbalos como binarios. En estos días, todos implementan el formato IEEE-754, por lo que siempre y cuando elija un orden de bytes y escriba o lea solo ese orden de bytes, los números serán portátiles.
  • Escríbalos como valores enteros de 64 bits. Aquí se puede utilizar la base habitual 10. (Porque usted está representando a la entero con 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 que contaría con una precisión perfecta (del software) aquí. Pero cualquier error será extremadamente pequeño y sus datos originales ciertamente no tienen información en los bits bajos. (Ninguna de las constantes de la física y la química se conocen a 52 bits, ni tiene ninguna la distancia en la tierra se ha medido a 52 bits de precisión.) Pero para una copia de seguridad o restauración donde la precisión bit a bit podría compararse automáticamente, esto obviamente no es ideal.

 17
Author: DigitalRoss,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-18 13:33:09

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

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

introduzca la descripción de la imagen aquí

Imagine 3 líneas numéricas:
1. para el número original de base 2
2. para la base redondeada 10 representación
3. para el número reconstruido (igual que #1 porque ambos en base 2)

Cuando convertir a base 10, gráficamente, se elige el tic en la 2ª línea numérica más cercana al tic en la 1ª. Igualmente cuando se reconstruye el original a partir del valor redondeado base 10.

La observación crítica que tuve fue que para permitir la reconstrucción exacta, el tamaño de paso de la base 10 (cuántica) tiene que ser

Tome el caso específico de cuando el exponente es 0 para la representación base2. Entonces el quantum base2 será 2^-52 ~= 2.22 * 10^-16. La base más cercana 10 cuántica que es menor que esto es 10^-16. Ahora que sabemos la base requerida 10 cuántica, ¿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 requerirían 17 dígitos (16 dígitos para la fracción y 1 dígito para la parte entera).

Para exponentes distintos de 0, podemos usar lo mismo 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 de que 17 dígitos son suficientes.

Espero que les guste mi explicación.

 9
Author: Yale Zhang,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-11-24 10:13:00

Un doble tiene la precisión de 52 dígitos binarios o 15,95 dígitos decimales. Véase 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 ver cuarta edición, abajo].

Por cierto, esto significa dígitos significativos.

Respuesta a las ediciones de OP:

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

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

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

El" paso " dentro del doble es:

.00000000000000000000000000000000000000000000000000000001 en binario.

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

De ahí la representación decimal anterior,

1.0000000000000002220446049250313 . . .

Es una representación inexacta del número binario exacto:

1.0000000000000000000000000000000000000000000000000001.

Tercera Edición:

El siguiente valor posible para un doble, que en el binario exacto is:

1.0000000000000000000000000000000000000000000000000010

Convierte inexactamente en decimal a

1.0000000000000004440892098500626 . . . .

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

Cuarta Edición:

Aunque un doble almacena como máximo 16 dígitos decimales significativos, a veces se necesitan 17 dígitos decimales para representar el número. El la razón tiene que ver con el corte de dígitos.

Como mencioné anteriormente, hay 52 + 1 dígitos binarios en el doble. El "+ 1 " es un 1 inicial asumido, 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 tal número? Bueno, log_10 (2^53-1) es aproximadamente 15.95. Así que a lo sumo 16 dígitos decimales son necesarios. Vamos a etiquetar estos d_0 a d_15.

Ahora considere que Los números de coma flotante IEEE también tienen un exponente binario. ¿Qué sucede cuando incrementamos el exponet, por ejemplo, por 2? Hemos multiplicado nuestro número de 52 bits, sea lo que sea, por 4. Ahora, en lugar de nuestros 52 dígitos binarios alineándose 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. Así que nuestros 15.95 dígitos decimales ahora ocuply d_1 a d_15, más algunos bits superiores de d_0 y algunos bits inferiores 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

 5
Author: ThomasMcLeod,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-31 17:10:06

La forma más fácil (para IEEE 754 double) 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.1000000000000001").

Un enfoque que ha funcionado 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 lo hace, pruebe con 16 dígitos. Si que no funciona, utilice 17.

Es posible Que desee probar El algoritmo de David Gay (usado en Python 3.1 para implementar float.__repr__).

 3
Author: dan04,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-01-20 01:22:16

Gracias a ThomasMcLeod por señalar el error en mi cálculo de tabla

Para garantizar la conversión de ida y vuelta utilizando 15 o 16 o 17 dígitos solo es posible para unos pocos casos comparativamente. El número 15.95 proviene de tomar 2^53 (1 bit implícito + 52 bits en el significand/"mantissa") que sale a un entero en el rango 10^15 a 10^16 (más cercano a 10^16).

Considere un valor de doble precisión x con un exponente de 0, es decir, cae en el punto flotante rango rango 1.0 -1 => 2^-1 o el componente 0.5.

El siguiente bit 0.25, los siguientes 0.125, 0.0625, 0.03125, 0.015625 y así sucesivamente (ver tabla a continuación). El valor 1.5 estará representado por dos componentes sumados: el bit implícito que denota 1.0 y el bit explícito significativo más alto que denota 0.5.

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

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
Author: Olof Forshell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-18 13:38:07

@ThomasMcLeod: Creo que la regla de los dígitos significativos proviene de mi campo, la física, y significa algo más sutil:

Si tienes una medida que te da el valor 1.52 y no puedes leer más detalles de la escala, y dices que se supone que debes agregar otro número (por ejemplo, de otra medida porque la escala de esta era demasiado pequeña), digamos 2, entonces el resultado (obviamente) tiene solo 2 decimales, es decir, 3.52. Pero del mismo modo, si agrega 1.1111111111 al valor 1.52, se obtiene el valor 2.63 (y nada más!).

La razón de la regla es evitar que te engañes a ti mismo haciéndote creer que obtuviste más información de un cálculo que la que pusiste en la medición (lo cual es imposible, pero parecería de esa manera llenándolo de basura, ver arriba).

Dicho esto, esta regla específica es solo para la adición (para la adición: el error del resultado es la suma de los dos errores-por lo que si mide solo uno mal, aunque la suerte, allí va su Precision...).

Cómo obtener las otras reglas: Digamos que a es el número medido y δa el error. Digamos que tu fórmula original era: f:= m a Digamos que también mide m con el error δm (que sea el lado positivo). Entonces el límite real es: f_up=(m + δm) (a + δa) y f_down=(m-δm) (a-δa) Tan, f_up = m a + δm δa+(δm a + m δa) f_down = m a + δm δa-(δm a + m δa) Por lo tanto, ahora los dígitos significativos son aún menos: 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, se obtiene: δf/f = δm / m + δa/a

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

Espero que llegue a la esencia y espero no haber cometido demasiados errores, es tarde aquí: -)

Tl,dr: Los dígitos significativos significan cuántos de los dígitos en la salida realmente provienen de los dígitos en su entrada (en el mundo real, no la imagen distorsionada que tienen los números de coma flotante). Si sus mediciones fueron 1 con error" no "y 3 con error" no " y la función se supone que 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, por lo que obviamente tienen que ser.

Si la regla de dígitos significativos significa algo completamente diferente en otro campo, continúe: -)

 0
Author: Danny Milosavljevic,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-26 22:50:30