
@rawpixel
Sogar Schulkinder sind sich der Existenz verschiedener Zahlensysteme und der Tatsache bewusst, dass nicht jeder endliche Dezimalbruch ein endlicher Bruch in einem binären Zahlensystem ist. Nur wenige Leute denken, dass aufgrund dieser Tatsache die Operationen auf Float und Double nicht genau sind.
Wenn wir über Erlang sprechen, implementiert es wie viele andere Sprachen den IEEE754-Standard für float, während der Standard-Integer-Typ in Erlang mit Arithmetik mit beliebiger Genauigkeit implementiert wird. Ich möchte jedoch nicht nur Bigint haben, sondern auch die Fähigkeit, mit rationalen, komplexen und Gleitkommazahlen mit der erforderlichen Genauigkeit zu arbeiten.
Der Artikel bietet einen minimalen Überblick über die Theorie der Codierung von Gleitkommazahlen und die auffälligsten Beispiele für neu auftretende Effekte. Die Lösung, die durch den Übergang zu einer Festkommadarstellung die erforderliche Genauigkeit der Operationen bietet, ist als EAPA-Bibliothek (Erlang Arbitrary Precision Arithmetic) konzipiert, die den Anforderungen von auf Erlang / Elixir entwickelten Finanzanwendungen gerecht wird.
Standards, Standards, Standards ...
Heute ist der IEEE754 der Hauptstandard für die binäre Gleitkomma-Arithmetik, der in der Technik und Programmierung weit verbreitet ist. Es definiert vier Präsentationsformate:
- 32 Bit mit einfacher Genauigkeit
- 64 Bit mit doppelter Genauigkeit
- einfach erweiterte Genauigkeit> = 43 Bit (selten verwendet)
- doppelte erweiterte Genauigkeit> = 79 Bit (normalerweise werden 80 Bit verwendet)
und vier Rundungsmodi: - Abrunden, auf das nächste Ganze achten.
- Rundung gegen Null.
- Rundung mit Tendenz zu + ∞
- Rundung in Richtung -∞
Die meisten modernen Mikroprozessoren werden mit Hardware-Implementierung der Darstellung realer Variablen im IEEE754-Format hergestellt. Präsentationsformate begrenzen die Größenbeschränkung einer Zahl, und Rundungsmodi wirken sich auf die Genauigkeit aus. Programmierer können das Verhalten von Hardware häufig nicht ändern und Programmiersprachen implementieren. Beispielsweise speichert die offizielle Erlang-Implementierung einen Float in 3 Wörtern auf einem 64-Bit-Computer und in 4 Wörtern auf einem 32-Bit-Computer.
Wie oben erwähnt, sind Zahlen im IEEE754-Format eine endliche Menge, auf die eine unendliche Menge von reellen Zahlen abgebildet wird, so dass die ursprüngliche Zahl im IEEE754-Format mit einem Fehler dargestellt werden kann.
Der Großteil der Zahlen, wenn sie auf einer endlichen Menge angezeigt werden, weist einen stabilen und kleinen relativen Fehler auf. Für Float sind es also 11,920928955078125e-6% und für Double 2,2204460492503130808472633361816e-14%. Im Leben von Programmierern erlauben uns die meisten alltäglichen Aufgaben, die gelöst werden können, diesen Fehler zu vernachlässigen, obwohl zu beachten ist, dass Sie selbst bei einfachen Aufgaben auf den Rechen treten können, da die Größe des absoluten Fehlers 10 31 und 10 292 für float bzw. double erreichen kann, was zu Berechnungsschwierigkeiten führt.
Effektillustration
Von allgemeinen Informationen bis zum Geschäft. Versuchen wir, die aufkommenden Effekte in Erlang zu reproduzieren.
Alle folgenden Beispiele sind als CT-Tests konzipiert.
Rundung und Genauigkeitsverlust
Beginnen wir mit den Klassikern - der Addition von zwei Zahlen: 0.1 + 0.2 =?:
t30000000000000004(_)-> ["0.30000000000000004"] = io_lib:format("~w", [0.1 + 0.2]).
Das Ergebnis der Zugabe unterscheidet sich geringfügig von dem intuitiv erwarteten, und der Test besteht erfolgreich. Versuchen wir, das richtige Ergebnis zu erzielen. Schreiben Sie den Test mit EAPA neu:
t30000000000000004_eapa(_)->
Dieser Test ist ebenfalls erfolgreich und zeigt, dass das Problem behoben wurde.
Lassen Sie uns die Experimente fortsetzen und einen sehr kleinen Wert zu 1.0 hinzufügen:
tiny(_)-> X = 1.0, Y = 0.0000000000000000000000001, 1.0 = X + Y.
Wie Sie sehen, blieb unser Anstieg unbemerkt. Wir versuchen, das Problem zu beheben und gleichzeitig eine der Funktionen der Bibliothek zu veranschaulichen - die automatische Skalierung:
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)).
Bitgitterüberlauf
Zusätzlich zu den Problemen, die mit kleinen Zahlen verbunden sind, ist ein Überlauf ein offensichtliches und bedeutendes Problem.
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.
Wie Sie dem Test entnehmen können, ist der Unterschied irgendwann nicht mehr gleich 1,0, was offensichtlich ein Problem ist. EAPA löst auch dieses Problem:
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)).
Gefährliche Reduzierung
Der folgende Test zeigt das Auftreten einer gefährlichen Reduktion. Dieser Prozess geht mit einer katastrophalen Abnahme der Genauigkeit von Berechnungen in Operationen einher, bei denen der resultierende Wert viel geringer als die Eingabe ist. In unserem Fall das Ergebnis der Subtraktion 1.
Wir zeigen, dass in Erlang dieses Problem vorliegt:
reduction(_)-> X = float(87654321098765432), Y = float(87654321098765431), 16.0 = XY.
Es stellte sich heraus, 16.0 anstelle der erwarteten 1.0. Versuchen wir, diese Situation zu beheben:
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)).
Weitere Merkmale der Gleitkomma-Arithmetik in Erlang
Beginnen wir damit, die negative Null zu ignorieren.
eq(_)-> true = list_to_float("0.0") =:= list_to_float("-0.0").
Ich möchte nur sagen, dass EAPA dieses Verhalten beibehält:
eq_eapa(_)-> X = eapa_int:with_val(1, <<"0.0">>), Y = eapa_int:with_val(1, <<"-0.0">>), true = eapa_int:eq(X, Y).
da es gültig ist. Erlang hat keine klare Syntax und Verarbeitung von NaN und Unendlichkeiten, was zu einer Reihe von Merkmalen führt, zum Beispiel:
1> math:sqrt(list_to_float("-0.0")). 0.0
Der nächste Punkt ist das Merkmal der Verarbeitung großer und kleiner Zahlen. Versuchen wir, für die Kleinen zu reproduzieren:
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
und für große Zahlen:
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
Hier noch ein paar Beispiele für kleine Zahlen:
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
Die obigen Beispiele bestätigen die Wahrheit für Erlang-Projekte: Geld kann in IEEE754 nicht gezählt werden.
EAPA (Erlang Arbitrary-Precision Arithmetic)
EAPA ist eine in Rust geschriebene NIF-Erweiterung. Derzeit bietet das EAPA-Repository die einfachste und bequemste eapa_int-Schnittstelle für die Arbeit mit Festkommazahlen. Die Funktionen von eapa_int umfassen Folgendes:
- Fehlende Auswirkungen der IEEE754-Codierung
- Unterstützung für große Zahlen
- Konfigurierbare Genauigkeit bis zu 126 Dezimalstellen. (in der aktuellen Implementierung)
- Autoskalierung
- Unterstützung für alle grundlegenden Operationen an Zahlen
- Mehr oder weniger vollständige Tests, einschließlich eigenschaftsbasierter Tests.
eapa_int
Schnittstelle:
with_val/2
- Übersetzung einer Gleitkommazahl in eine feste Darstellung, die auch sicher in json, xml verwendet werden kann.to_float/2
- Übersetzung einer Festkommazahl in eine Gleitkommazahl mit einer bestimmten Genauigkeit.to_float/1
- übersetzt eine Festkommazahl in eine Gleitkommazahl.add/2
- die Summe zweier Zahlensub/2
- Unterschiedmul/2
- Multiplikationdivp/2
- Divisionmin/2
- das Minimum an Zahlenmax/2
- das Maximum der Zahleneq/2
- Gleichheit der Zahlen prüfenlt/2
- Überprüfen Sie, ob die Anzahl geringer istlte/2
- Überprüfung weniger als gleichgt/2
- Überprüfen Sie, ob die Anzahl größer istgte/2
- Überprüfung ist mehr als gleich
EAPA-Code finden Sie im Repository https://github.com/Vonmo/eapa
Wann sollten Sie eapa_int verwenden? Wenn Ihre Anwendung beispielsweise mit Geld arbeitet oder Sie Berechnungsvorgänge für Zahlen wie 92233720368547758079223372036854775807.92233720368547758079223372036854775807 bequem und genau ausführen müssen, können Sie EAPA sicher verwenden.
Wie jede Lösung ist EAPA ein Kompromiss. Wir erhalten die erforderliche Genauigkeit, indem wir auf Speicher und Rechengeschwindigkeit verzichten. Leistungstests und Statistiken, die auf realen Systemen gesammelt wurden, zeigen, dass die meisten Operationen im Bereich von 3 bis 30 μs ausgeführt werden. Dieser Punkt muss auch bei der Auswahl einer EAPA-Festkomma-Schnittstelle berücksichtigt werden.
Fazit
Natürlich ist es bei weitem nicht immer notwendig, solche Probleme mit Erlang oder Elixir zu lösen, aber wenn ein Problem auftritt und kein geeignetes Werkzeug gefunden wird, müssen Sie eine Lösung erfinden.
Dieser Artikel ist ein Versuch, das Tool und die Erfahrung mit der Community zu teilen, in der Hoffnung, dass diese Bibliothek für einige Benutzer nützlich ist und Zeit spart.
Was denkst du über Geld in Erlang?
PS Die Arbeit mit rationalen und komplexen Zahlen sowie der native Zugriff auf Integer-, Float-, Complex- und Rational-Typen mit beliebiger Genauigkeit werden in den folgenden Veröffentlichungen behandelt. Nicht wechseln!
Verwandte Materialien: