Arithmétique de précision arbitraire à Erlang


@rawpixel


Même les écoliers sont conscients de l'existence de divers systèmes de nombres et du fait que toutes les fractions décimales finies ne sont pas des fractions finies dans un système de nombres binaires. Peu de gens pensent que de ce fait, les opérations sur float et double ne sont pas exactes.


Si nous parlons d'Erlang, alors, comme beaucoup d'autres langages, il implémente la norme IEEE754 pour float, tandis que le type Integer standard dans Erlang est implémenté en utilisant une arithmétique de précision arbitraire. Cependant, je voudrais avoir non seulement le bigint, mais aussi la capacité de fonctionner avec des nombres rationnels, complexes et à virgule flottante avec la précision nécessaire.


L'article fournit un aperçu minimal de la théorie du codage des nombres à virgule flottante et les exemples les plus frappants d'effets émergents. La solution qui fournit la précision nécessaire des opérations à travers la transition vers une représentation en virgule fixe est conçue comme une bibliothèque EAPA (Erlang Arbitrary Precision Arithmetic), conçue pour répondre aux besoins des applications financières développées sur Erlang / Elixir.




Normes, normes, normes ...


Aujourd'hui, la principale norme pour l'arithmétique à virgule flottante binaire est l'IEEE754, largement utilisé en ingénierie et en programmation. Il définit quatre formats de présentation:


  • 32 bits simple précision
  • 64 bits double précision
  • précision simple étendue> = 43 bits (rarement utilisé)
  • précision à double extension> = 79 bits (généralement 80 bits utilisés)
    et quatre modes d'arrondi:
  • Arrondi, tendant vers l'entier le plus proche.
  • Arrondi tendant vers zéro.
  • Arrondi tendant vers + ∞
  • Arrondi vers -∞

La plupart des microprocesseurs modernes sont fabriqués avec une implémentation matérielle de la représentation des variables réelles au format IEEE754. Les formats de présentation limitent la taille limite d'un nombre et les modes d'arrondi affectent la précision. Les programmeurs ne peuvent souvent pas modifier le comportement du matériel et implémenter des langages de programmation. Par exemple, l'implémentation officielle d'Erlang stocke un flottant en 3 mots sur une machine 64 bits et en 4 mots sur une machine 32 bits.


Comme mentionné ci-dessus, les nombres au format IEEE754 sont un ensemble fini sur lequel un ensemble infini de nombres réels est mappé, de sorte que le numéro d'origine peut être présenté au format IEEE754 avec une erreur.


La majeure partie des nombres lorsqu'ils sont affichés sur un ensemble fini a une petite erreur relative stable. Ainsi, pour le flotteur, il est de 11,920928955078125e-6% et pour le double - 2,2204460492503130808472633361816e-14%. Dans la vie des programmeurs, la plupart des tâches quotidiennes à résoudre nous permettent de négliger cette erreur, même s'il convient de noter que même dans les tâches simples, vous pouvez marcher sur le râteau, car l'ampleur de l'erreur absolue peut atteindre 10 31 et 10 292 pour float et double, respectivement, ce qui provoque des difficultés de calcul.


Illustration des effets


De l'information générale aux affaires. Essayons de reproduire les effets émergents dans Erlang.


Tous les exemples ci-dessous sont conçus comme des tests ct.


Arrondi et perte de précision


Commençons par les classiques - l'ajout de deux nombres: 0,1 + 0,2 = ?:


t30000000000000004(_)-> ["0.30000000000000004"] = io_lib:format("~w", [0.1 + 0.2]). 

Le résultat de l'addition est légèrement différent de celui attendu intuitivement et le test réussit. Essayons d'obtenir le bon résultat. Réécrivez le test en utilisant 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)). 

Ce test réussit également, montrant que le problème a été résolu.
Continuons les expériences, ajoutons une très petite valeur à 1.0:


 tiny(_)-> X = 1.0, Y = 0.0000000000000000000000001, 1.0 = X + Y. 

Comme vous pouvez le constater, notre augmentation est passée inaperçue. Nous essayons de résoudre le problème, illustrant simultanément l'une des fonctionnalités de la bibliothèque - la mise à l'échelle automatique:


 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)). 

Débordement de la grille de bits


En plus des problèmes associés aux petits nombres, le débordement est un problème évident et significatif.


 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. 

Comme vous pouvez le voir dans le test, à un moment donné, la différence cesse d'être égale à 1,0, ce qui est évidemment un problème. L'EAPA résout également ce problème:


 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)). 

Réduction dangereuse


Le test suivant montre l'occurrence d'une réduction dangereuse. Ce processus s'accompagne d'une diminution catastrophique de la précision des calculs dans les opérations où la valeur résultante est bien inférieure à l'entrée. Dans notre cas, le résultat de la soustraction 1.


Nous montrons qu'à Erlang ce problème est présent:


 reduction(_)-> X = float(87654321098765432), Y = float(87654321098765431), 16.0 = XY. %% has to be 1.0 

Il s'est avéré 16,0 au lieu du 1,0 attendu. Essayons de résoudre cette situation:


 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)). 

Autres caractéristiques de l'arithmétique en virgule flottante à Erlang


Commençons par ignorer le zéro négatif.


 eq(_)-> true = list_to_float("0.0") =:= list_to_float("-0.0"). 

Je veux juste dire que l'EAPA conserve ce comportement:


 eq_eapa(_)-> X = eapa_int:with_val(1, <<"0.0">>), Y = eapa_int:with_val(1, <<"-0.0">>), true = eapa_int:eq(X, Y). 

car il est valide. Erlang n'a pas de syntaxe et de traitement clairs du NaN et des infinis, ce qui donne lieu à un certain nombre de fonctionnalités, par exemple:


 1> math:sqrt(list_to_float("-0.0")). 0.0 

Le point suivant est la caractéristique du traitement des grands et des petits nombres. Essayons de reproduire pour les plus petits:


 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 

et pour les grands nombres:


 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 

Voici quelques exemples supplémentaires pour les petits nombres:


 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 

Les exemples ci-dessus confirment la vérité pour les projets Erlang: l'argent ne peut pas être compté dans IEEE754.


EAPA (Erlang Arbitrary-Precision Arithmetic)


EAPA est une extension NIF écrite en rouille. À l'heure actuelle, le référentiel EAPA fournit l'interface eapa_int la plus simple et la plus pratique pour travailler avec des nombres à virgule fixe. Les fonctionnalités de eapa_int sont les suivantes:


  1. Manque d'effets du codage IEEE754
  2. Prise en charge des grands nombres
  3. Précision configurable jusqu'à 126 décimales. (en cours de mise en œuvre)
  4. Mise à l'échelle automatique
  5. Prise en charge de toutes les opérations de base sur les numéros
  6. Tests plus ou moins complets, y compris basés sur la propriété.

Interface eapa_int :


  • with_val/2 - traduction d'un nombre à virgule flottante en une représentation fixe, qui peut être utilisée, y compris en toute sécurité, en json, xml.
  • to_float/2 - traduction d'un nombre à virgule fixe en nombre à virgule flottante avec une précision donnée.
  • to_float/1 - traduire un nombre à virgule fixe en nombre à virgule flottante.
  • add/2 - la somme de deux nombres
  • sub/2 - différence
  • mul/2 - multiplication
  • divp/2 - division
  • min/2 - le minimum de nombres
  • max/2 - le maximum des nombres
  • eq/2 - vérifier l'égalité des nombres
  • lt/2 - vérifiez que le nombre est inférieur
  • lte/2 - vérification inférieure à égale
  • gt/2 - vérifier que le nombre est supérieur
  • gte/2 - la vérification est plus que égale

Le code EAPA se trouve dans le référentiel https://github.com/Vonmo/eapa


Quand devriez-vous utiliser eapa_int? Par exemple, si votre application fonctionne avec de l'argent ou si vous devez effectuer des opérations de calcul de manière pratique et précise sur des nombres tels que 92233720368547758079223372036854775807.92233720368547758079223372036854775807, vous pouvez utiliser l'EAPA en toute sécurité.


Comme toute solution, l'EAPA est un compromis. Nous obtenons la précision nécessaire en sacrifiant la mémoire et la vitesse de calcul.Les tests de performance et les statistiques collectés sur des systèmes réels montrent que la plupart des opérations sont effectuées dans la plage de 3 à 30 μs. Ce point doit également être pris en compte lors du choix d'une interface à point fixe EAPA.


Conclusion


Bien sûr, il est loin d'être toujours nécessaire de résoudre de tels problèmes sur Erlang ou Elixir, mais lorsqu'un problème survient et qu'un outil adapté n'est pas trouvé, il faut inventer une solution.
Cet article est une tentative de partager l'outil et l'expérience avec la communauté, dans l'espoir que pour certaines personnes cette bibliothèque sera utile et aidera à gagner du temps.
Que pensez-vous de l'argent à Erlang?


PS Le travail avec des nombres rationnels et complexes, ainsi que l'accès natif aux types de précision arbitraire Integer, Float, Complex, Rational, seront traités dans les publications suivantes. Ne changez pas!




Matériaux associés:


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


All Articles