
@rawpixel
Até as crianças em idade escolar estão cientes da existência de vários sistemas numéricos e do fato de que nem toda fração decimal finita é uma fração finita em um sistema numérico binário. Poucas pessoas pensam que, devido a esse fato, as operações em float e double não sejam exatas.
Se falamos sobre Erlang, então, como muitas outras linguagens, ele implementa o padrão IEEE754 para float, enquanto o tipo inteiro padrão em Erlang é implementado usando aritmética de precisão arbitrária. No entanto, gostaria de ter não apenas bigint, mas também a capacidade de operar com números racionais, complexos e de ponto flutuante com a precisão necessária.
O artigo fornece uma visão geral mínima da teoria da codificação de números de ponto flutuante e os exemplos mais impressionantes de efeitos emergentes. A solução que fornece a precisão necessária das operações através da transição para uma representação de ponto fixo é projetada como uma biblioteca EAPA (Aritmética de Precisão Arbitrária Erlang), projetada para atender às necessidades de aplicativos financeiros desenvolvidos em Erlang / Elixir.
Padrões, normas, normas ...
Hoje, o principal padrão para aritmética binária de ponto flutuante é o IEEE754, amplamente utilizado em engenharia e programação. Ele define quatro formatos de apresentação:
- precisão única de 32 bits
- precisão dupla de 64 bits
- precisão estendida única> = 43 bits (raramente usada)
- precisão estendida dupla> = 79 bits (geralmente 80 bits usados)
e quatro modos de arredondamento: - Arredondamento, tendendo para o todo mais próximo.
- Arredondamento tendendo a zero.
- Arredondamento tendendo a + ∞
- Arredondando para -∞
Os microprocessadores mais modernos são fabricados com a implementação de hardware da representação de variáveis reais no formato IEEE754. Os formatos de apresentação limitam o tamanho do número e os modos de arredondamento afetam a precisão. Os programadores geralmente não podem alterar o comportamento do hardware e implementar linguagens de programação. Por exemplo, a implementação oficial do Erlang armazena um float em 3 palavras em uma máquina de 64 bits e em 4 palavras em uma máquina de 32 bits.
Como mencionado acima, os números no formato IEEE754 são um conjunto finito no qual um conjunto infinito de números reais é mapeado; portanto, o número original pode ser apresentado no formato IEEE754 com um erro.
A maior parte dos números, quando exibidos em um conjunto finito, apresenta um erro relativo estável e pequeno. Portanto, para float é 11.920928955078125e-6% e para dobro - 2.2204460492503130808472633361816e-14%. Na vida dos programadores, a maioria das tarefas diárias a serem resolvidas nos permite negligenciar esse erro, embora deva-se observar que mesmo em tarefas simples você pode pisar no rake, já que a magnitude do erro absoluto pode chegar a 10 31 e 10 292 para flutuar e dobrar, respectivamente, causando dificuldades nos cálculos.
Ilustração de efeitos
De informações gerais a negócios. Vamos tentar reproduzir os efeitos emergentes em Erlang.
Todos os exemplos abaixo são projetados como testes ct.
Arredondamento e perda de precisão
Vamos começar com os clássicos - a adição de dois números: 0,1 + 0,2 = ?:
t30000000000000004(_)-> ["0.30000000000000004"] = io_lib:format("~w", [0.1 + 0.2]).
O resultado da adição é um pouco diferente do esperado intuitivamente, e o teste passa com sucesso. Vamos tentar alcançar o resultado certo. Reescreva o teste usando EAPA:
t30000000000000004_eapa(_)->
Este teste também foi bem-sucedido, mostrando que o problema foi resolvido.
Vamos continuar os experimentos, adicionar um valor muito pequeno para 1.0:
tiny(_)-> X = 1.0, Y = 0.0000000000000000000000001, 1.0 = X + Y.
Como você pode ver, nosso aumento passou despercebido. Estamos tentando corrigir o problema, ilustrando simultaneamente um dos recursos da biblioteca - escala automática:
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)).
Excesso de grade de bits
Além dos problemas associados a números pequenos, o excesso é um problema óbvio e 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 você pode ver no teste, em algum momento a diferença deixa de ser igual a 1,0, o que é obviamente um problema. A EAPA também resolve esse 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)).
Redução perigosa
O teste a seguir demonstra a ocorrência de uma redução perigosa. Esse processo é acompanhado por uma diminuição catastrófica na precisão dos cálculos nas operações em que o valor resultante é muito menor que o valor de entrada. No nosso caso, o resultado da subtração 1.
Mostramos que em Erlang esse problema está presente:
reduction(_)-> X = float(87654321098765432), Y = float(87654321098765431), 16.0 = XY.
Descobriu-se 16.0 em vez do esperado 1.0. Vamos tentar corrigir esta situação:
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)).
Outras características da aritmética de ponto flutuante em Erlang
Vamos começar ignorando o zero negativo.
eq(_)-> true = list_to_float("0.0") =:= list_to_float("-0.0").
Só quero dizer que o EAPA mantém esse comportamento:
eq_eapa(_)-> X = eapa_int:with_val(1, <<"0.0">>), Y = eapa_int:with_val(1, <<"-0.0">>), true = eapa_int:eq(X, Y).
uma vez que é válido. Erlang não possui uma sintaxe e processamento claros de NaN e infinitos, o que dá origem a vários recursos, por exemplo, estes:
1> math:sqrt(list_to_float("-0.0")). 0.0
O próximo ponto é a característica do processamento de números grandes e pequenos. Vamos tentar reproduzir para os mais pequenos:
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
e 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
Aqui estão mais alguns exemplos para números pequenos:
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
Os exemplos acima confirmam a verdade para os projetos Erlang: o dinheiro não pode ser contado no IEEE754.
EAPA (Aritmética de precisão arbitrária de Erlang)
EAPA é uma extensão NIF escrita em Rust. No momento, o repositório EAPA fornece a interface eapa_int mais simples e conveniente para trabalhar com números de ponto fixo. Os recursos de eapa_int incluem o seguinte:
- Falta de efeitos da codificação IEEE754
- Suporte para grandes números
- Precisão configurável até 126 casas decimais. (na implementação atual)
- Escalonamento automático
- Suporte para todas as operações básicas em números
- Teste mais ou menos completo, incluindo a propriedade.
Interface eapa_int
:
with_val/2
- tradução de um número de ponto flutuante em uma representação fixa, que pode ser usada, incluindo com segurança, em json, xml.to_float/2
- conversão de um número de ponto fixo em um número de ponto flutuante com uma determinada precisão.to_float/1
- converte um número de ponto fixo em um número de ponto flutuante.add/2
- a soma de dois númerossub/2
- diferençamul/2
- multiplicaçãodivp/2
- divisãomin/2
- o mínimo de númerosmax/2
- o máximo dos númeroseq/2
- verifica a igualdade de númeroslt/2
- verifique se o número é menorlte/2
- verificando menos que igualgt/2
- verifique se o número é maiorgte/2
- a verificação é mais que igual
O código EAPA pode ser encontrado no repositório https://github.com/Vonmo/eapa
Quando você deve usar eapa_int? Por exemplo, se seu aplicativo funcionar com dinheiro ou você precisar executar operações computacionais de maneira conveniente e precisa em números como 92233720368547758079223372036854775807.92233720368547758079223372036854775807, você poderá usar o EAPA com segurança.
Como qualquer solução, o EAPA é um compromisso. Obtemos a precisão necessária sacrificando a memória e a velocidade computacional.Os testes de desempenho e as estatísticas coletadas em sistemas reais mostram que a maioria das operações é realizada na faixa de 3-30 μs. Este ponto também deve ser considerado ao escolher uma interface de ponto fixo EAPA.
Conclusão
Obviamente, nem sempre é necessário resolver esses problemas no Erlang ou no Elixir, mas quando surge um problema e uma ferramenta adequada não é encontrada, você precisa inventar uma solução.
Este artigo é uma tentativa de compartilhar com a comunidade a ferramenta e a experiência, na esperança de que para algumas pessoas essa biblioteca seja útil e ajude a economizar tempo.
O que você acha do dinheiro em Erlang?
PS O trabalho com números racionais e complexos, bem como o acesso nativo aos tipos inteiro, flutuante, complexo e racional de precisão arbitrária serão abordados nas publicações a seguir. Não mude!
Materiais relacionados: