
@rawpixel
Incluso los escolares son conscientes de la existencia de varios sistemas numéricos y del hecho de que no todas las fracciones decimales finitas son una fracción finita en un sistema numérico binario. Pocas personas piensan que debido a este hecho, las operaciones en flotante y doble no son exactas.
Si hablamos de Erlang, entonces, como muchos otros idiomas, implementa el estándar IEEE754 para flotante, mientras que el tipo entero estándar en Erlang se implementa utilizando aritmética de precisión arbitraria. Sin embargo, me gustaría tener no solo bigint, sino también la capacidad de operar con números racionales, complejos y de coma flotante con la precisión necesaria.
El artículo proporciona una visión general mínima de la teoría de codificación de números de coma flotante y los ejemplos más sorprendentes de efectos emergentes. La solución que proporciona la precisión necesaria de las operaciones a través de la transición a una representación de punto fijo está diseñada como una biblioteca EAPA (Erlang Arbitrary Precision Arithmetic), diseñada para satisfacer las necesidades de las aplicaciones financieras desarrolladas en Erlang / Elixir.
Normas, normas, normas ...
Hoy, el estándar principal para la aritmética de punto flotante binario es el IEEE754, ampliamente utilizado en ingeniería y programación. Define cuatro formatos de presentación:
- precisión simple 32 bits
- doble precisión de 64 bits
- precisión de extensión única> = 43 bits (rara vez se usa)
- precisión de doble extensión> = 79 bits (normalmente se utilizan 80 bits)
y cuatro modos de redondeo: - Redondeando, tendiendo al entero más cercano.
- Redondeo tendiente a cero.
- Redondeo tendiente a + ∞
- Redondeando hacia -∞
La mayoría de los microprocesadores modernos se fabrican con una implementación de hardware de la representación de variables reales en el formato IEEE754. Los formatos de presentación limitan el límite de tamaño de un número, y los modos de redondeo afectan la precisión. Los programadores a menudo no pueden cambiar el comportamiento del hardware e implementar lenguajes de programación. Por ejemplo, la implementación oficial de Erlang almacena un flotante en 3 palabras en una máquina de 64 bits y en 4 palabras en una de 32 bits.
Como se mencionó anteriormente, los números en el formato IEEE754 son un conjunto finito en el que se asigna un conjunto infinito de números reales, por lo que el número original puede presentarse en el formato IEEE754 con un error.
La mayor parte de los números cuando se muestran en un conjunto finito tiene un error relativo estable y pequeño. Entonces, para flotación es 11.920928955078125e-6%, y para doble - 2.2204460492503130808472633361816e-14%. En la vida de los programadores, la mayoría de las tareas diarias que se pueden resolver nos permiten descuidar este error, aunque debe tenerse en cuenta que incluso en tareas simples puede pisar el rastrillo, ya que la magnitud del error absoluto puede alcanzar 10 31 y 10 292 para flotación y doble, respectivamente, causando dificultades en los cálculos.
Ilustración de efectos
De la información general a los negocios. Intentemos reproducir los efectos emergentes en Erlang.
Todos los ejemplos a continuación están diseñados como pruebas ct.
Redondeo y pérdida de precisión.
Comencemos con los clásicos: la suma de dos números: 0.1 + 0.2 = ?:
t30000000000000004(_)-> ["0.30000000000000004"] = io_lib:format("~w", [0.1 + 0.2]).
El resultado de la adición es ligeramente diferente del intuitivo esperado, y la prueba pasa con éxito. Tratemos de lograr el resultado correcto. Reescribe la prueba usando EAPA:
t30000000000000004_eapa(_)->
Esta prueba también tiene éxito, mostrando que el problema ha sido resuelto.
Continuemos los experimentos, agreguemos un valor muy pequeño a 1.0:
tiny(_)-> X = 1.0, Y = 0.0000000000000000000000001, 1.0 = X + Y.
Como puede ver, nuestro aumento pasó desapercibido. Estamos tratando de solucionar el problema, ilustrando simultáneamente una de las características de la biblioteca: el escalado automático:
tiny_eapa(_)-> X1 = eapa_int:with_val(1, <<"1.0">>), X2 = eapa_int:with_val(25, <<"0.0000000000000000000000001">>), <<"1.0000000000000000000000001">> = eapa_int:to_float(eapa_int:add(X1, X2)).
Bit Grid Overflow
Además de los problemas asociados con números pequeños, el desbordamiento es un problema obvio y significativo.
float_overflow(_) -> 1.0 = 9007199254740991.0 - 9007199254740990.0, 1.0 = 9007199254740992.0 - 9007199254740991.0, 0.0 = 9007199254740993.0 - 9007199254740992.0, 2.0 = 9007199254740994.0 - 9007199254740993.0.
Como puede ver en la prueba, en algún momento la diferencia deja de ser igual a 1.0, lo que obviamente es un problema. EAPA también resuelve este problema:
float_overflow_eapa(_)-> X11 = eapa_int:with_val(1, <<"9007199254740992.0">>), X21 = eapa_int:with_val(1, <<"9007199254740991.0">>), <<"1.0">> = eapa_int:to_float(1, eapa_int:sub(X11, X21)), X12 = eapa_int:with_val(1, <<"9007199254740993.0">>), X22 = eapa_int:with_val(1, <<"9007199254740992.0">>), <<"1.0">> = eapa_int:to_float(1, eapa_int:sub(X12, X22)), X13 = eapa_int:with_val(1, <<"9007199254740994.0">>), X23 = eapa_int:with_val(1, <<"9007199254740993.0">>), <<"1.0">> = eapa_int:to_float(1, eapa_int:sub(X13, X23)).
Reducción peligrosa
La siguiente prueba demuestra la ocurrencia de una reducción peligrosa. Este proceso va acompañado de una disminución catastrófica en la precisión de los cálculos en operaciones donde el valor resultante es mucho menor que la entrada. En nuestro caso, el resultado de la resta 1.
Mostramos que en Erlang este problema está presente:
reduction(_)-> X = float(87654321098765432), Y = float(87654321098765431), 16.0 = XY.
Resultó 16.0 en lugar del esperado 1.0. Intentemos solucionar esta situación:
reduction_eapa(_)-> X = eapa_int:with_val(1, <<"87654321098765432">>), Y = eapa_int:with_val(1, <<"87654321098765431">>), <<"1.0">> = eapa_int:to_float(eapa_int:sub(X, Y)).
Otras características de la aritmética de coma flotante en Erlang
Comencemos ignorando el cero negativo.
eq(_)-> true = list_to_float("0.0") =:= list_to_float("-0.0").
Solo quiero decir que EAPA conserva este comportamiento:
eq_eapa(_)-> X = eapa_int:with_val(1, <<"0.0">>), Y = eapa_int:with_val(1, <<"-0.0">>), true = eapa_int:eq(X, Y).
Ya que es válido. Erlang no tiene una sintaxis y un procesamiento claros de NaN e infinitos, lo que da lugar a una serie de características, por ejemplo, estas:
1> math:sqrt(list_to_float("-0.0")). 0.0
El siguiente punto es la característica de procesar números grandes y pequeños. Tratemos de reproducir para los pequeños:
2> list_to_float("0."++lists:duplicate(322, $0)++"1"). 1.0e-323 3> list_to_float("0."++lists:duplicate(323, $0)++"1"). 0.0
y para grandes números:
4> list_to_float("1"++lists:duplicate(308, $0)++".0"). 1.0e308 5> list_to_float("1"++lists:duplicate(309, $0)++".0"). ** exception error: bad argument
Aquí hay un par de ejemplos más para números pequeños:
6> list_to_float("0."++lists:duplicate(322, $0)++"123456789"). 1.0e-323 7> list_to_float("0."++lists:duplicate(300, $0)++"123456789"). 1.23456789e-301
8> 0.123456789e-100 * 0.123456789e-100. 1.524157875019052e-202 9> 0.123456789e-200 * 0.123456789e-200. 0.0
Los ejemplos anteriores confirman la verdad para los proyectos de Erlang: el dinero no se puede contar en IEEE754.
EAPA (Aritmética de precisión arbitraria de Erlang)
EAPA es una extensión NIF escrita en Rust. Por el momento, el repositorio EAPA proporciona la interfaz eapa_int más simple y conveniente para trabajar con números de punto fijo. Las características de eapa_int incluyen lo siguiente:
- Falta de efectos de la codificación IEEE754
- Soporte de grandes números
- Precisión configurable hasta 126 decimales. (en implementación actual)
- Autoescalado
- Soporte para todas las operaciones básicas en números.
- Pruebas más o menos completas, incluidas las basadas en propiedades.
Interfaz eapa_int
:
with_val/2
: traducción de un número de coma flotante en una representación fija, que se puede utilizar, incluso de forma segura, en json, xml.to_float/2
: traducción de un número de punto fijo a un número de punto flotante con una precisión dada.to_float/1
- traduce un número de punto fijo a un número de punto flotante.add/2
- la suma de dos númerossub/2
- diferenciamul/2
- multiplicacióndivp/2
- divisiónmin/2
- el mínimo de númerosmax/2
- el máximo de los númeroseq/2
- verificar igualdad de númeroslt/2
- verifica que el número sea menorlte/2
- comprobación menos que igualgt/2
- verifica que el número sea mayorgte/2
: la comprobación es más que igual
El código EAPA se puede encontrar en el repositorio https://github.com/Vonmo/eapa
¿Cuándo deberías usar eapa_int? Por ejemplo, si su aplicación funciona con dinero o necesita realizar operaciones computacionales de manera conveniente y precisa en números como 92233720368547758079223372036854775807.92233720368547758079223372036854775807, puede usar EAPA de manera segura.
Como cualquier solución, EAPA es un compromiso. Obtenemos la precisión necesaria al sacrificar la memoria y la velocidad de cálculo.Las pruebas de rendimiento y las estadísticas recopiladas en sistemas reales muestran que la mayoría de las operaciones se realizan en el rango de 3-30 μs. Este punto también debe tenerse en cuenta al elegir una interfaz de punto fijo EAPA.
Conclusión
Por supuesto, está lejos de ser siempre necesario resolver tales problemas en Erlang o Elixir, pero cuando surge un problema y no se encuentra una herramienta adecuada, debe inventar una solución.
Este artículo es un intento de compartir con la comunidad la herramienta y la experiencia, con la esperanza de que para algunas personas esta biblioteca sea útil y ayude a ahorrar tiempo.
¿Qué opinas sobre el dinero en Erlang?
PD El trabajo con números racionales y complejos, así como el acceso nativo a números enteros, flotantes, complejos y racionales de precisión arbitraria se tratarán en las siguientes publicaciones. ¡No cambies!
Materiales relacionados: