math - una - rotacion matematicas




¿Es rota la matemática de punto flotante? (18)

Considere el siguiente código:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

¿Por qué ocurren estas inexactitudes?


Perspectiva de un diseñador de hardware

Creo que debería agregar la perspectiva de un diseñador de hardware a esto, ya que diseño y construyo hardware de punto flotante. Conocer el origen del error puede ayudar a comprender lo que está sucediendo en el software y, en última instancia, espero que esto ayude a explicar las razones por las que ocurren los errores de punto flotante y que se acumulen con el tiempo.

1. Información general

Desde una perspectiva de ingeniería, la mayoría de las operaciones de punto flotante tendrán algún elemento de error, ya que el hardware que realiza los cálculos de punto flotante solo debe tener un error de menos de la mitad de una unidad en el último lugar. Por lo tanto, gran parte del hardware se detendrá con una precisión que solo es necesaria para producir un error de menos de la mitad de una unidad en el último lugar para una sola operación, lo cual es especialmente problemático en la división de punto flotante. Lo que constituye una sola operación depende de cuántos operandos tome la unidad. Para la mayoría, son dos, pero algunas unidades toman 3 o más operandos. Debido a esto, no hay garantía de que las operaciones repetidas resulten en un error deseable ya que los errores se acumulan con el tiempo.

2. Normas

La mayoría de los procesadores siguen el estándar IEEE-754 , pero algunos usan estándares no normalizados o diferentes. Por ejemplo, hay un modo desnormalizado en IEEE-754 que permite la representación de números de punto flotante muy pequeños a expensas de la precisión. Sin embargo, lo siguiente cubrirá el modo normalizado de IEEE-754, que es el modo de operación típico.

En el estándar IEEE-754, a los diseñadores de hardware se les permite cualquier valor de error / épsilon siempre y cuando sea menos de la mitad de una unidad en el último lugar, y el resultado solo tiene que ser menos de la mitad de una unidad en el último lugar para una operación. Esto explica por qué cuando hay operaciones repetidas, los errores se suman. Para la precisión doble IEEE-754, este es el bit 54, ya que se usan 53 bits para representar la parte numérica (normalizada), también llamada mantisa, del número de punto flotante (por ejemplo, 5.3 en 5.3e5). Las siguientes secciones detallan las causas del error de hardware en varias operaciones de punto flotante.

3. Causa del error de redondeo en la división

La principal causa del error en la división de punto flotante son los algoritmos de división utilizados para calcular el cociente. La mayoría de los sistemas informáticos calculan la división utilizando la multiplicación por una inversa, principalmente en Z=X/Y , Z = X * (1/Y) . Una división se calcula de forma iterativa, es decir, cada ciclo calcula algunos bits del cociente hasta que se alcanza la precisión deseada, lo que para IEEE-754 es cualquier cosa con un error de menos de una unidad en el último lugar. La tabla de recíprocos de Y (1 / Y) se conoce como la tabla de selección de cociente (QST) en la división lenta, y el tamaño en bits de la tabla de selección de cociente es generalmente el ancho del radix, o un número de bits de el cociente calculado en cada iteración, más unos pocos bits de guarda. Para el estándar IEEE-754, doble precisión (64 bits), sería el tamaño de la base del divisor, más algunos bits de guarda k, donde k>=2 . Así, por ejemplo, una Tabla de selección de cociente típica para un divisor que calcula 2 bits del cociente a la vez (raíz 4) sería 2+2= 4 bits (más unos pocos bits opcionales).

3.1 Error de redondeo de división: aproximación de recíproco

Los recíprocos de la tabla de selección de cocientes dependen del método de división : división lenta, como la división SRT, o división rápida, como la división Goldschmidt; Cada entrada se modifica de acuerdo con el algoritmo de división en un intento de producir el error más bajo posible. En cualquier caso, sin embargo, todos los recíprocos son aproximaciones del recíproco real e introducen algún elemento de error. Tanto los métodos de división lenta como los de división rápida calculan el cociente de manera iterativa, es decir, se calcula una cantidad de bits del cociente en cada paso, luego el resultado se resta del dividendo y el divisor repite los pasos hasta que el error es menor que la mitad de uno. Unidad en el último lugar. Los métodos de división lenta calculan un número fijo de dígitos del cociente en cada paso y suelen ser menos costosos de construir, y los métodos de división rápida calculan un número variable de dígitos por paso y generalmente son más caros de construir. La parte más importante de los métodos de división es que la mayoría de ellos se basan en la multiplicación repetida por una aproximación recíproca, por lo que son propensos a errores.

4. Redondeo de errores en otras operaciones: truncamiento

Otra causa de los errores de redondeo en todas las operaciones son los diferentes modos de truncamiento de la respuesta final que permite IEEE-754. Hay truncado, redondeado hacia cero, redondeo al más cercano (predeterminado), redondeo hacia abajo y redondeo hacia arriba. Todos los métodos introducen un elemento de error de menos de una unidad en el último lugar para una sola operación. Con el tiempo y las operaciones repetidas, el truncamiento también se suma al error resultante. Este error de truncamiento es especialmente problemático en la exponenciación, que implica alguna forma de multiplicación repetida.

5. Operaciones repetidas

Dado que el hardware que realiza los cálculos de punto flotante solo necesita producir un resultado con un error de menos de la mitad de una unidad en el último lugar para una sola operación, el error aumentará con las operaciones repetidas si no se observa. Esta es la razón por la que en los cálculos que requieren un error acotado, los matemáticos utilizan métodos como el uso del dígito par redondeado más cercano en el último lugar de IEEE-754, porque, con el tiempo, es más probable que los errores se cancelen entre sí. out, y la aritmética de intervalos combinada con variaciones de los modos de redondeo IEEE 754 para predecir errores de redondeo y corregirlos. Debido a su bajo error relativo en comparación con otros modos de redondeo, redondear al dígito par más cercano (en el último lugar), es el modo de redondeo predeterminado de IEEE-754.

Tenga en cuenta que el modo de redondeo predeterminado, el dígito par redondeado más cercano en el último lugar , garantiza un error de menos de la mitad de una unidad en el último lugar para una operación. Usar el truncamiento, redondear y redondear solo puede dar como resultado un error que sea mayor que la mitad de una unidad en el último lugar, pero menor que una unidad en el último lugar, por lo que estos modos no se recomiendan a menos que sean utilizado en aritmética de intervalos.

6. Resumen

En resumen, la razón fundamental de los errores en las operaciones de punto flotante es una combinación del truncamiento en el hardware y el truncamiento de un recíproco en el caso de la división. Dado que el estándar IEEE-754 solo requiere un error de menos de la mitad de una unidad en el último lugar para una sola operación, los errores de punto flotante sobre las operaciones repetidas se acumularán a menos que se corrijan.


No, no roto, pero la mayoría de las fracciones decimales deben ser aproximadas

Resumen

La aritmética de punto flotante es exacta, desafortunadamente, no coincide bien con nuestra representación habitual del número base-10, por lo que resulta que a menudo le estamos dando una entrada que está ligeramente alejada de lo que escribimos.

Incluso los números simples como 0.01, 0.02, 0.03, 0.04 ... 0.24 no se pueden representar exactamente como fracciones binarias. Si cuentas hasta 0.01, .02, .03 ..., no hasta que llegues a 0.25 obtendrás la primera fracción representable en la base 2 . Si intentara usar FP, su 0.01 habría estado ligeramente apagado, por lo que la única forma de sumar 25 de ellos hasta un buen valor exacto de 0.25 habría requerido una larga cadena de causalidad que involucra brocas de protección y redondeo. Es difícil de predecir, así que levantamos nuestras manos y decimos que "FP es inexacto", pero eso no es realmente cierto.

Constantemente le damos al hardware de FP algo que parece simple en la base 10 pero es una fracción que se repite en la base 2.

¿Cómo pasó esto?

Cuando escribimos en decimal, cada fracción (específicamente, cada decimal de terminación) es un número racional de la forma

a / (2 n x 5 m )

En binario, solo obtenemos el término 2 n , es decir:

a / 2 n

Así que en decimal, no podemos representar 1 / 3 . Debido a que la base 10 incluye 2 como factor primordial, cada número que podamos escribir como una fracción binaria también se puede escribir como una fracción base 10. Sin embargo, casi nada de lo que escribimos como fracción base 10 es representable en binario. En el rango de 0.01, 0.02, 0.03 ... 0.99, solo tres números se pueden representar en nuestro formato de FP: 0.25, 0.50 y 0.75, porque son 1/4, 1/2 y 3/4, todos los números con un factor primo usando solo el término 2 n .

En la base 10 que no podemos representar 1 / 3 . Pero en binario, no podemos hacer 1 / 10 o 1 / 3 .

Entonces, mientras que cada fracción binaria se puede escribir en decimal, lo contrario no es cierto. Y de hecho la mayoría de las fracciones decimales se repiten en binario.

Lidiando con eso

Los desarrolladores generalmente reciben instrucciones para hacer comparaciones de <epsilon , un mejor consejo podría ser redondear a valores integrales (en la biblioteca C: round () y roundf (), es decir, permanecer en el formato de PF) y luego comparar. El redondeo a una longitud de fracción decimal específica resuelve la mayoría de los problemas con la salida.

Además, en los problemas reales de procesamiento de números (los problemas para los que se inventó la FP en computadoras tempranas y terriblemente caras) las constantes físicas del universo y todas las demás mediciones solo se conocen en un número relativamente pequeño de cifras significativas, por lo que todo el espacio del problema era "inexacto" de todos modos. La "precisión" de FP no es un problema en este tipo de aplicación.

Todo el problema surge realmente cuando las personas intentan usar FP para el conteo de frijoles. Funciona para eso, pero solo si se adhiere a los valores integrales, qué tipo de derrotas el punto de usarlo. Es por eso que tenemos todas esas bibliotecas de software de fracción decimal.

Me encanta la respuesta de Pizza de , porque describe el problema real, no solo el habitual gesto de "inexactitud". Si la PF fuera simplemente "inexacta", podríamos solucionarlo y lo habríamos hecho décadas atrás. La razón por la que no lo hemos hecho es porque el formato FP es compacto y rápido y es la mejor manera de procesar muchos números. Además, es un legado de la era espacial y la carrera de armamentos y los primeros intentos de resolver grandes problemas con computadoras muy lentas que usan sistemas de memoria pequeña. (A veces, núcleos magnéticos individuales para almacenamiento de 1 bit, pero esa es otra historia ) .

Conclusión

Si solo está contando beans en un banco, las soluciones de software que utilizan representaciones de cadenas decimales en primer lugar funcionan perfectamente bien. Pero no puedes hacer cromodinámica cuántica o aerodinámica de esa manera.


Cuando convierte .1 o 1/10 a base 2 (binario), obtiene un patrón de repetición después del punto decimal, al igual que tratar de representar 1/3 en la base 10. El valor no es exacto y, por lo tanto, no puede hacerlo Matemáticas exactas con él usando métodos normales de punto flotante


Errores de redondeo de punto flotante. 0.1 no se puede representar con la misma precisión en la base-2 como en la base-10 debido al factor primo faltante de 5. Al igual que 1/3 toma un número infinito de dígitos para representar en decimal, pero es "0.1" en la base-3, 0.1 toma un número infinito de dígitos en base-2 donde no lo hace en base-10. Y las computadoras no tienen una cantidad infinita de memoria.


Matemáticas de punto flotante binario es así. En la mayoría de los lenguajes de programación, se basa en el estándar IEEE 754 . JavaScript utiliza una representación de punto flotante de 64 bits, que es lo mismo que el double de Java. El quid del problema es que los números se representan en este formato como un número entero por una potencia de dos; los números racionales (como 0.1 , que es 1/10 ) cuyo denominador no es una potencia de dos no se pueden representar exactamente.

Para 0.1 en el formato binary64 estándar, la representación se puede escribir exactamente como

  • 0.1000000000000000055511151231257827021181583404541015625 en decimal, o
  • 0x1.999999999999ap-4 en la notación hexfloat C99 .

En contraste, el número racional 0.1 , que es 1/10 , puede escribirse exactamente como

  • 0.1 en decimal, o
  • 0x1.99999999999999...p-4 en un análogo de la notación hexfloat C99, donde ... representa una secuencia interminable de 9.

Las constantes 0.2 y 0.3 en su programa también serán aproximaciones a sus valores reales. Sucede que el double más cercano a 0.2 es más grande que el número racional 0.2 pero que el double más cercano a 0.3 es más pequeño que el número racional 0.3 . La suma de 0.1 y 0.2 termina siendo mayor que el número racional 0.3 y, por lo tanto, no está de acuerdo con la constante en su código.

Un tratamiento bastante completo de los problemas aritméticos de punto flotante es lo que todo científico informático debe saber sobre la aritmética de punto flotante . Para una explicación más fácil de digerir, vea floating-point-gui.de .


¿Probaste la solución de cinta adhesiva?

Trate de determinar cuándo ocurren los errores y corríjalos con frases cortas, no es bonito, pero para algunos problemas es la única solución y esta es una de ellas.

 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}    

Tuve el mismo problema en un proyecto de simulación científica en c #, y puedo decirte que si ignoras el efecto mariposa, se convertirá en un gran dragón gordo y te morderá en el a **


Mi respuesta es bastante larga, así que la dividí en tres secciones. Dado que la pregunta es sobre las matemáticas de punto flotante, he puesto énfasis en lo que realmente hace la máquina. También lo hice específico para precisión doble (64 bits), pero el argumento se aplica igualmente a cualquier aritmética de punto flotante.

Preámbulo

Un número de formato de punto flotante binario de doble precisión IEEE 754 (binary64) representa un número del formulario

valor = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023

en 64 bits:

  • El primer bit es el bit de signo : 1 si el número es negativo, 0 caso contrario 1 .
  • Los siguientes 11 bits son el exponent , que está offset por 1023. En otras palabras, después de leer los bits del exponente de un número de precisión doble, se debe restar 1023 para obtener la potencia de dos.
  • Los 52 bits restantes son el significand (o mantisa). En la mantisa, un 1 'implícito' siempre se omite 2 ya que el bit más significativo de cualquier valor binario es 1 .

1 - IEEE 754 permite el concepto de cero con signo - +0 y -0 se tratan de manera diferente: 1 / (+0) es infinito positivo; 1 / (-0) es infinito negativo. Para valores cero, los bits de mantisa y exponente son todos cero. Nota: los valores cero (+0 y -0) no se clasifican explícitamente como denormal 2 .

2 - Este no es el caso de los números denormales , que tienen un exponente de desplazamiento de cero (y un 0. implícito). El rango de los números de doble precisión denormal es d min ≤ | x | ≤ d max , donde d min (el número distinto de cero representable más pequeño) es 2 -1023 - 51 (≈ 4.94 * 10 -324 ) y d max (el número denormal más grande, para el cual la mantisa consta de 1 s) es 2 - 1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).

Convertir un número de doble precisión a binario.

Existen muchos convertidores en línea para convertir un número de punto flotante de doble precisión a binario (por ejemplo, en binaryconvert.com ), pero aquí hay un código de C # de muestra para obtener la representación IEEE 754 para un número de doble precisión (separo las tres partes con dos puntos ( : ):

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Llegando al punto: la pregunta original.

(Ir a la parte inferior de la versión TL; DR)

(el que pregunta) preguntó por qué 0.1 + 0.2! = 0.3.

Escritas en binario (con dos puntos separando las tres partes), las representaciones IEEE 754 de los valores son:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Tenga en cuenta que la mantisa se compone de dígitos recurrentes de 0011 . Esto es clave para explicar por qué hay algún error en los cálculos: 0.1, 0.2 y 0.3 no se pueden representar en binario precisamente en un número finito de bits binarios, como máximo 1/9, 1/3 o 1/7 se pueden representar precisamente en dígitos decimales

Al convertir los exponentes a decimales, eliminar el desplazamiento y volver a agregar el 1 implícito (entre corchetes), 0.1 y 0.2 son:

0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010

Para sumar dos números, el exponente debe ser el mismo, es decir:

0.1 = 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 = 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111

Dado que la suma no es de la forma 2 n * 1. {bbb} aumentamos el exponente en uno y cambiamos el punto decimal ( binario ) para obtener:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

Ahora hay 53 bits en la mantisa (la 53 se encuentra entre corchetes en la línea de arriba). El modo de redondeo predeterminado para IEEE 754 es ' Redondear al más cercano ', es decir, si un número x se encuentra entre dos valores a y b , se elige el valor donde el bit menos significativo es cero.

a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

Tenga en cuenta que a y b difieren solo en el último bit; ...0011 + 1 = ...0100 . En este caso, el valor con el bit menos significativo de cero es b , por lo que la suma es:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

TL; DR

Escribiendo 0.1 + 0.2 en una representación binaria IEEE 754 (con dos puntos separando las tres partes) y comparándolo con 0.3 , esto es (he puesto los distintos bits entre corchetes):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Convertido de nuevo a decimal, estos valores son:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

La diferencia es exactamente 2 -54 , que es ~ 5.5511151231258 × 10 -17 - insignificante (para muchas aplicaciones) en comparación con los valores originales.

La comparación de los últimos bits de un número de punto flotante es intrínsecamente peligrosa, como lo sabrá cualquiera que lea " Lo que todo científico informático debe saber sobre la aritmética de punto flotante " (que cubre todas las partes principales de esta respuesta).

La mayoría de las calculadoras usan dígitos de guarda adicionales para solucionar este problema, que es cómo 0.1 + 0.2 darían 0.3 : los pocos bits finales se redondean.


Math.sum (javascript) .... tipo de reemplazo de operador

.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001
Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value), 
                p = Math.max(precision, 0) || 0, 
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number + "")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    diff:{
        value: function(A,B){
            var prec = this.max(this.get_precision(A),this.get_precision(B));
            return +this.precision(A-B,prec);
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

la idea es utilizar operadores matemáticos en lugar de evitar errores de flotación

Math.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false

También tenga en cuenta que Math.diff y Math.sum detectan automáticamente la precisión a utilizar

Math.sum acepta cualquier número de argumentos


Algunas estadísticas relacionadas con esta famosa pregunta de doble precisión.

Al agregar todos los valores ( a + b ) usando un paso de 0.1 (de 0.1 a 100) tenemos un 15% de probabilidad de error de precisión . Tenga en cuenta que el error podría resultar en valores ligeramente más grandes o más pequeños. Aquí hay unos ejemplos:

0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

Al restar todos los valores ( a - b donde a> b ) usando un paso de 0.1 (de 100 a 0.1) tenemos un 34% de probabilidad de error de precisión . Aquí hay unos ejemplos:

0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

* El 15% y el 34% son realmente enormes, por lo que siempre use BigDecimal cuando la precisión sea de gran importancia. Con 2 dígitos decimales (paso 0.01) la situación empeora un poco más (18% y 36%).


Dado que este hilo se ramificó un poco en una discusión general sobre las implementaciones actuales de punto flotante, agregaría que hay proyectos para solucionar sus problemas.

Eche un vistazo a https://posithub.org/ por ejemplo, que muestra un tipo de número llamado posit (y su predecesor unum) que promete ofrecer una mejor precisión con menos bits. Si mi comprensión es correcta, también corrige el tipo de problemas en la pregunta. Proyecto bastante interesante, la persona detrás de él es un matemático, el Dr. John Gustafson . Todo es de código abierto, con muchas implementaciones reales en C / C ++, Python, Julia y C # ( https://hastlayer.com/arithmetics ).


Esos números extraños aparecen porque las computadoras usan un sistema numérico binario (base 2) para fines de cálculo, mientras que nosotros usamos el decimal (base 10).

Hay una mayoría de números fraccionarios que no se pueden representar precisamente en binario o en decimal o en ambos. Resultado: un número redondeado (pero preciso) de resultados.


Mi solución:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

precisión se refiere al número de dígitos que desea conservar después del punto decimal durante la suma.


Solo por diversión, jugué con la representación de flotadores, siguiendo las definiciones del Estándar C99 y escribí el siguiente código.

El código imprime la representación binaria de flotadores en 3 grupos separados

SIGN EXPONENT FRACTION

y después de eso, imprime una suma que, cuando se suma con suficiente precisión, mostrará el valor que realmente existe en el hardware.

Por lo tanto, cuando escriba float x = 999..., el compilador transformará ese número en una representación de bits impresa por la función de xxtal manera que la suma impresa por la función yysea ​​igual al número dado.

En realidad, esta suma es sólo una aproximación. Para el número 999,999,999 el compilador insertará en la representación de bits del flotador el número 1,000,000,000.

Después del código, adjunto una sesión de consola, en la que computo la suma de los términos para ambas constantes (menos PI y 999999999) que realmente existen en el hardware, insertadas allí por el compilador.

#include <stdio.h>
#include <limits.h>

void
xx(float *x)
{
    unsigned char i = sizeof(*x)*CHAR_BIT-1;
    do {
        switch (i) {
        case 31:
             printf("sign:");
             break;
        case 30:
             printf("exponent:");
             break;
        case 23:
             printf("fraction:");
             break;

        }
        char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
        printf("%d ", b);
    } while (i--);
    printf("\n");
}

void
yy(float a)
{
    int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
    int fraction = ((1<<23)-1)&(*(int*)&a);
    int exponent = (255&((*(int*)&a)>>23))-127;

    printf(sign?"positive" " ( 1+":"negative" " ( 1+");
    unsigned int i = 1<<22;
    unsigned int j = 1;
    do {
        char b=(fraction&i)!=0;
        b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
    } while (j++, i>>=1);

    printf("*2^%d", exponent);
    printf("\n");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu\n", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

Aquí hay una sesión de consola en la que calculo el valor real del flotador que existe en el hardware. Solía bcimprimir la suma de los términos emitidos por el programa principal. Uno puede insertar esa suma en python replo algo similar también.

-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872

Eso es. El valor de 999999999 es de hecho

999999999.999999446351872

También puedes verificar bcque -3.14 también está perturbado. No te olvides de establecer un scalefactor en bc.

La suma mostrada es lo que está dentro del hardware. El valor que obtenga al calcularlo depende de la escala que establezca. Establecí el scalefactor a 15. Matemáticamente, con una precisión infinita, parece que es 1,000,000,000.


Una pregunta diferente ha sido nombrada como un duplicado a este:

En C ++, ¿por qué el resultado es cout << xdiferente del valor que muestra un depurador x?

El xen la pregunta es una floatvariable.

Un ejemplo sería

float x = 9.9F;

El depurador muestra 9.89999962, la salida de la coutoperación es 9.9.

La respuesta es que coutla precisión por defecto floates 6, por lo que se redondea a 6 dígitos decimales.

Vea here para referencia


El tipo de matemática de punto flotante que se puede implementar en una computadora digital necesariamente usa una aproximación de los números reales y las operaciones en ellos. (La versión estándar tiene más de cincuenta páginas de documentación y tiene un comité para tratar sus erratas y su refinamiento adicional).

Esta aproximación es una mezcla de aproximaciones de diferentes tipos, cada una de las cuales puede ignorarse o explicarse cuidadosamente debido a su forma específica de desviación de la exactitud. También involucra una cantidad de casos excepcionales explícitos tanto en el nivel de hardware como de software que la mayoría de las personas pasan por delante mientras fingen no darse cuenta.

Si necesita una precisión infinita (por ejemplo, utilizando el número π, en lugar de uno de sus muchos más cortos suplentes), debe escribir o usar un programa matemático simbólico.

Pero si está de acuerdo con la idea de que, a veces, las matemáticas de punto flotante son confusas en valor y la lógica y los errores se pueden acumular rápidamente, y puede escribir sus requisitos y pruebas para permitirlo, entonces su código con frecuencia puede subsistir con el contenido su FPU.


Error de redondeo de punto flotante. De lo que todo científico informático debe saber sobre la aritmética de punto flotante :

Exprimir infinitos números reales en un número finito de bits requiere una representación aproximada. Aunque hay infinitos enteros, en la mayoría de los programas el resultado de los cálculos de enteros se puede almacenar en 32 bits. En contraste, dado un número fijo de bits, la mayoría de los cálculos con números reales producirán cantidades que no se pueden representar exactamente usando tantos bits. Por lo tanto, el resultado de un cálculo de punto flotante a menudo se debe redondear para encajar en su representación finita. Este error de redondeo es el rasgo característico del cálculo de punto flotante.


Otra forma de ver esto: se usan 64 bits para representar números. Como consecuencia, no hay forma de que se representen con precisión más de 2 ** 64 = 18,446,744,073,709,551,616 números diferentes.

Sin embargo, Math dice que ya hay infinitos decimales entre 0 y 1. IEE 754 define una codificación para usar estos 64 bits de manera eficiente para un espacio numérico mucho más grande más NaN e +/- Infinito, por lo que hay espacios entre los números representados con precisión y llenos solo números aproximados.

Lamentablemente 0,3 se encuentra en un hueco.


Para ofrecer la mejor solución , puedo decir que descubrí el siguiente método:

parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

Déjame explicarte por qué es la mejor solución. Como otros mencionados en las respuestas anteriores, es una buena idea usar la función toFixed () de listo para usar para resolver el problema. Pero lo más probable es que te encuentres con algunos problemas.

Imagínese que usted va a sumar dos números flotantes como 0.2y 0.7aquí está: 0.2 + 0.7 = 0.8999999999999999.

Su resultado esperado fue 0.9que significa que necesita un resultado con una precisión de 1 dígito en este caso. Así que deberías haber usado, (0.2 + 0.7).tofixed(1)pero no puedes simplemente darle un determinado parámetro a ToFixed () ya que depende del número dado, por ejemplo

`0.22 + 0.7 = 0.9199999999999999`

En este ejemplo, necesita una precisión de 2 dígitos, por lo que debería ser toFixed(2), entonces, ¿cuál debería ser el parámetro para ajustarse a cada número flotante dado?

Se podría decir que sea 10 en cada situación, entonces:

(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"

¡Maldita sea! ¿Qué vas a hacer con esos ceros no deseados después de 9? Es el momento de convertirlo en flotador para hacerlo como desees:

parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

Ahora que ha encontrado la solución, es mejor ofrecerla como una función como esta:

function floatify(number){
           return parseFloat((number).toFixed(10));
        }

Intentémoslo usted mismo:

function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>

Puedes usarlo de esta manera:

var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

Como W3SCHOOLS sugiere que también hay otra solución, puedes multiplicar y dividir para resolver el problema anterior:

var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

¡Tenga en cuenta que (0.2 + 0.1) * 10 / 10no funcionará en absoluto aunque parezca lo mismo! Prefiero la primera solución ya que puedo aplicarla como una función que convierte el flotador de entrada en un flotador de salida preciso.





floating-accuracy