PHP - Precisión de números flotantes [duplicado]
$a = '35';
$b = '-34.99';
echo ($a + $b);
Resultados en 0,009999999999998
¿Qué pasa con eso? Me preguntaba por qué mi programa seguía informando resultados extraños.
¿Por qué PHP no devuelve el 0,01 esperado?
Porque aritmética de punto flotante! = aritmética de números reales. Un ejemplo de la diferencia debida a la imprecisión es, para algunos flotadores a
y b
, (a+b)-b != a
. Esto se aplica a cualquier idioma que utilice flotantes.
Dado que el punto flotante son números binarios con precisión finita, hay una cantidad finita de números representables , lo que genera problemas de precisión y sorpresas como esta. Aquí hay otra lectura interesante: Lo que todo informático debería saber sobre la aritmética de punto flotante .
Volviendo a su problema, básicamente no hay manera de representar con precisión 34,99 o 0,01 en binario (al igual que en decimal, 1/3 = 0,3333...), por lo que en su lugar se utilizan aproximaciones. Para solucionar el problema, puede:
Úselo
round($result, 2)
en el resultado para redondearlo a 2 decimales.Utilice números enteros. Si se trata de moneda, digamos dólares estadounidenses, entonces almacene $35,00 como 3500 y $34,99 como 3499, luego divida el resultado entre 100.
Es una pena que PHP no tenga un tipo de datos decimal como otros lenguajes .
Los números de coma flotante, como todos los números, deben almacenarse en la memoria como una cadena de 0 y 1. Son todos bits para la computadora. La diferencia entre el punto flotante y el número entero es cómo interpretamos los 0 y los 1 cuando queremos verlos.
Un bit es el "signo" (0 = positivo, 1 = negativo), 8 bits son el exponente (que van de -128 a +127), 23 bits son el número conocido como "mantisa" (fracción). Entonces la representación binaria de (S1)(P8)(M23) tiene el valor (-1^S)M*2^P
La "mantisa" adquiere una forma especial. En notación científica normal mostramos el "lugar de uno" junto con la fracción. Por ejemplo:
4,39x10^2 = 439
En binario, el "lugar de uno" es un solo bit. Dado que ignoramos todos los ceros más a la izquierda en notación científica (ignoramos cualquier cifra insignificante), se garantiza que el primer bit será un 1.
1,101x2^3 = 1101 = 13
Como tenemos la garantía de que el primer bit será un 1, eliminamos este bit al almacenar el número para ahorrar espacio. Entonces, el número anterior se almacena solo como 101 (para la mantisa). Se supone que el 1 principal
Como ejemplo, tomemos la cadena binaria
00000010010110000000000000000000
Dividiéndolo en sus componentes:
Sign Power Mantissa
0 00000100 10110000000000000000000
+ +4 1.1011
+ +4 1 + .5 + .125 + .0625
+ +4 1.6875
Aplicando nuestra sencilla fórmula:
(-1^S)M*2^P
(-1^0)(1.6875)*2^(+4)
(1)(1.6875)*(16)
27
En otras palabras, 00000010010110000000000000000000 es 27 en coma flotante (según los estándares IEEE-754).
Sin embargo, para muchos números no existe una representación binaria exacta. Al igual que 1/3 = 0,333.... repitiéndose para siempre, 1/100 es 0,00000010100011110101110000..... con un "10100011110101110000" repetido. Sin embargo, una computadora de 32 bits no puede almacenar el número completo en punto flotante. Entonces hace su mejor suposición.
0.0000001010001111010111000010100011110101110000
Sign Power Mantissa
+ -7 1.01000111101011100001010
0 -00000111 01000111101011100001010
0 11111001 01000111101011100001010
01111100101000111101011100001010
(tenga en cuenta que el 7 negativo se produce usando el complemento a 2)
Debería quedar inmediatamente claro que 01111100101000111101011100001010 no se parece en nada a 0,01
Sin embargo, lo más importante es que contiene una versión truncada de un decimal periódico. El decimal original contenía una repetición "10100011110101110000". Hemos simplificado esto a 01000111101011100001010
Al traducir este número de coma flotante nuevamente a decimal mediante nuestra fórmula, obtenemos 0.0099999979 (tenga en cuenta que esto es para una computadora de 32 bits. Una computadora de 64 bits tendría mucha más precisión)
Un equivalente decimal
Si ayuda a comprender mejor el problema, veamos la notación científica decimal cuando trabajemos con decimales periódicos.
Supongamos que tenemos 10 "cuadros" para almacenar dígitos. Por tanto si quisiéramos almacenar un número como 1/16 escribiríamos:
+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
+---+---+---+---+---+---+---+---+---+---+
Lo cual es claramente justo 6.25 e -2
, donde e
está la abreviatura de *10^(
. Hemos asignado 4 casillas para el decimal aunque solo necesitábamos 2 (relleno con ceros), y hemos asignado 2 casillas para los signos (una para el signo del número, otra para el signo del exponente)
Usando 10 cuadros como este podemos mostrar números que van desde -9.9999 e -9
hasta+9.9999 e +9
Esto funciona bien para cualquier cosa con 4 decimales o menos, pero ¿qué sucede cuando intentamos almacenar un número como 2/3
?
+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
Este nuevo número 0.66667
no es exactamente igual 2/3
. De hecho, está fuera de 0.000003333...
. Si intentáramos escribir 0.66667
en base 3, obtendríamos 0.2000000000012...
en lugar de0.2
Este problema puede volverse más evidente si tomamos algo con un decimal periódico más grande, como 1/7
. Tiene 6 dígitos repetidos:0.142857142857...
Al almacenar esto en nuestra computadora decimal, solo podemos mostrar 5 de estos dígitos:
+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
Este número, 0.14286
está desviado por.000002857...
Es "casi correcto", pero no es exactamente correcto , por lo que si intentáramos escribir este número en base 7 obtendríamos un número horrible en lugar de 0.1
. De hecho, al conectar esto a Wolfram Alpha obtenemos:.10000022320335...
Estas pequeñas diferencias fraccionarias deberían resultarle familiares 0.0099999979
(a diferencia de 0.01
)
Aquí hay muchas respuestas sobre por qué los números de coma flotante funcionan como lo hacen...
Pero se habla poco de precisión arbitraria (Pickle lo mencionó). Si desea (o necesita) precisión exacta, la única forma de hacerlo (al menos para números racionales) es usar la extensión BC Math (que en realidad es solo una implementación de BigNum, Arbitrary Precision ...
Para sumar dos números:
$number = '12345678901234.1234567890';
$number2 = '1';
echo bcadd($number, $number2);
resultará en 12345678901235.1234567890
...
Esto se llama matemática de precisión arbitraria. Básicamente, todos los números son cadenas que se analizan para cada operación y las operaciones se realizan dígito por dígito (piense en una división larga, pero la realiza la biblioteca). Eso significa que es bastante lento (en comparación con las construcciones matemáticas normales). Pero es muy poderoso. Puedes multiplicar, sumar, restar, dividir, encontrar módulo y exponenciar cualquier número que tenga una representación de cadena exacta.
Por lo tanto, no puede hacerlo 1/3
con una precisión del 100%, ya que tiene un decimal periódico (y por lo tanto no es racional).
Pero, si quieres saber qué 1500.0015
es el cuadrado:
El uso de flotantes de 32 bits (doble precisión) da el resultado estimado de:
2250004.5000023
Pero bcmath da la respuesta exacta de:
2250004.50000225
Todo depende de la precisión que necesites.
Además, algo más a tener en cuenta aquí. PHP sólo puede representar enteros de 32 o 64 bits (dependiendo de su instalación). Entonces, si un número entero excede el tamaño del tipo int nativo (2,1 mil millones para 32 bits, 9,2 x10^18 o 9,2 mil millones para enteros con signo), PHP convertirá el int en un flotante. Si bien eso no es un problema inmediato (dado que todos los enteros más pequeños que la precisión del flotante del sistema son, por definición, directamente representables como flotantes), si intenta multiplicar dos juntos, perderá una precisión significativa.
Por ejemplo, dado $n = '40000000002'
:
Como número, $n
será float(40000000002)
, lo cual está bien ya que está exactamente representado. Pero si lo elevamos al cuadrado obtenemos:float(1.60000000016E+21)
Como cadena (usando matemáticas BC), $n
será exactamente '40000000002'
. Y si lo elevamos al cuadrado obtenemos: string(22) "1600000000160000000004"
...
Entonces, si necesita precisión con números grandes o puntos decimales racionales, es posible que desee consultar bcmath...
bcadd() podría resultar útil aquí.
<?PHP
$a = '35';
$b = '-34.99';
echo $a + $b;
echo '<br />';
echo bcadd($a,$b,2);
?>
(salida ineficiente para mayor claridad)
La primera línea me da 0,009999999999998. El segundo me da 0.01
Porque 0,01 no se puede representar exactamente como la suma de una serie de fracciones binarias. Y así es como se almacenan los flotadores en la memoria.
Supongo que no es lo que quieres escuchar, pero es la respuesta a una pregunta. Para saber cómo solucionarlo, consulte otras respuestas.