Aritmética da precisão arbitrária em Erlang


@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(_)-> %% prec = 1 symbols after coma X = eapa_int:with_val(1, <<"0.1">>), Y = eapa_int:with_val(1, <<"0.2">>), <<"0.3">> = eapa_int:to_float(1, eapa_int:add(X, Y)). 

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. %% has to be 1.0 

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:


  1. Falta de efeitos da codificação IEEE754
  2. Suporte para grandes números
  3. Precisão configurável até 126 casas decimais. (na implementação atual)
  4. Escalonamento automático
  5. Suporte para todas as operações básicas em números
  6. 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úmeros
  • sub/2 - diferença
  • mul/2 - multiplicação
  • divp/2 - divisão
  • min/2 - o mínimo de números
  • max/2 - o máximo dos números
  • eq/2 - verifica a igualdade de números
  • lt/2 - verifique se o número é menor
  • lte/2 - verificando menos que igual
  • gt/2 - verifique se o número é maior
  • gte/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:


Source: https://habr.com/ru/post/pt428111/


All Articles