Das Missverständnis der Gleitkomma-Arithmetik und ihrer Mängel ist eine Hauptursache für Überraschung und Verwirrung in der Programmierung. Angesichts der Tatsache, dass viele Programmierer die Auswirkungen noch nicht verstanden haben, besteht die Möglichkeit, dass viele subtile Fehler (insbesondere in Finanzsoftware) auftreten. Was können Programmiersprachen tun, um ihre Fallstricke für diejenigen zu vermeiden, die mit den Konzepten nicht vertraut sind, und gleichzeitig ihre Geschwindigkeit anzubieten, wenn Genauigkeit für diejenigen, die die Konzepte verstehen, nicht kritisch ist ?
language-design
Adam Paynter
quelle
quelle
Antworten:
Sie sagen, "speziell für Finanzsoftware", was einen meiner Lieblingsbeschwerden hervorruft: Geld ist kein Float, es ist ein Int .
Klar, es sieht aus wie ein Schwimmer. Dort steht ein Dezimalpunkt. Aber das liegt nur daran, dass Sie an Einheiten gewöhnt sind, die das Problem verwirren. Geld kommt immer in ganzzahligen Mengen. In Amerika sind es Cent. (In bestimmten Zusammenhängen denke ich, dass es Mühlen sein können , aber ignorieren Sie das fürs Erste.)
Wenn Sie also 1,23 Dollar sagen, sind das wirklich 123 Cent. Immer, immer, immer rechnen, und es wird dir gut gehen. Weitere Informationen finden Sie unter:
Bei der direkten Beantwortung der Frage sollten Programmiersprachen nur einen Money-Typ als vernünftiges Grundelement enthalten.
aktualisieren
Ok, ich hätte nur zweimal "immer" sagen sollen, nicht dreimal. Geld ist in der Tat immer ein int; diejenigen, die anders denken, können gerne versuchen, mir 0,3 Cent zu schicken und mir das Ergebnis auf Ihrem Kontoauszug zu zeigen. Kommentatoren weisen jedoch darauf hin, dass es seltene Ausnahmen gibt, wenn Sie Gleitkomma-Berechnungen für geldähnliche Zahlen durchführen müssen. ZB bestimmte Arten von Preisen oder Zinsberechnungen. Auch dann sollten diese wie Ausnahmen behandelt werden. Geld kommt rein und geht raus als ganzzahlige Mengen. Je näher Ihr System dem kommt, desto vernünftiger wird es.
quelle
Decimal
ist das einzig vernünftige System, um damit umzugehen, und Ihr Kommentar "Ignoriere das jetzt" ist der Vorbote des Schicksals für Programmierer überall: PIn vielen Fällen hilft es, Unterstützung für einen Dezimaltyp bereitzustellen. Viele Sprachen haben einen Dezimaltyp, werden jedoch nicht ausreichend verwendet.
Es ist wichtig, die Annäherung zu verstehen, die beim Arbeiten mit der Darstellung reeller Zahlen auftritt. Die Verwendung von Dezimal- und Gleitkommatypen
9 * (1/9) != 1
ist eine korrekte Aussage. Bei Konstanten kann ein Optimierer die Berechnung so optimieren, dass sie korrekt ist.Die Angabe eines ungefähren Operators würde helfen. Solche Vergleiche sind jedoch problematisch. Beachten Sie, dass .9999 Billionen Dollar ungefähr 1 Billion Dollar entsprechen. Könnten Sie bitte die Differenz auf mein Bankkonto einzahlen?
quelle
0.9999...
Billionen Dollar entsprechen genau 1 Billion Dollar.0.99999...
. Sie werden alle irgendwann abgeschnitten, was zu einer Ungleichung führt.0.9999
ist gleich genug für das Engineering. Aus finanziellen Gründen ist es nicht.Uns wurde gesagt, was wir im ersten Jahr (im zweiten Jahr) in der Informatik zu tun haben, als ich zur Universität ging (dieser Kurs war auch eine Voraussetzung für die meisten naturwissenschaftlichen Kurse).
Ich erinnere mich an den Vortragenden, der sagte: "Gleitkommazahlen sind Näherungswerte. Verwenden Sie Ganzzahlen für Geld. Verwenden Sie FORTRAN oder eine andere Sprache mit BCD-Zahlen für eine genaue Berechnung." (Und dann wies er auf die Näherung hin, wobei er das klassische Beispiel von 0,2 verwendete, das in binären Gleitkommazahlen nicht genau dargestellt werden konnte.) Dies zeigte sich auch in dieser Woche in den Laborübungen.
Dieselbe Vorlesung: "Wenn Sie mehr Genauigkeit durch Fließkommazahlen erzielen möchten, sortieren Sie Ihre Begriffe. Addieren Sie kleine Zahlen und nicht große Zahlen." Das ist mir in den Sinn gekommen.
Vor ein paar Jahren hatte ich eine sphärische Geometrie, die sehr genau und trotzdem schnell sein musste. 80-Bit-Double auf PCs schnitt nicht ab, daher habe ich dem Programm einige Typen hinzugefügt, die die Begriffe sortierten, bevor kommutative Operationen ausgeführt wurden. Problem gelöst.
Bevor Sie sich über die Qualität der Gitarre beschweren, lernen Sie, zu spielen.
Ich hatte vor vier Jahren einen Kollegen, der für JPL gearbeitet hatte. Er drückte seinen Unglauben aus, dass wir FORTRAN für einige Dinge verwendet haben. (Wir brauchten sehr genaue numerische Simulationen, die offline berechnet wurden.) "Wir haben das ganze FORTRAN durch C ++ ersetzt", sagte er stolz. Ich fragte mich nicht mehr, warum sie einen Planeten verpasst hatten.
quelle
1.0 + 0.1 + ... + 0.1
(10-mal wiederholt) wird zurückgegeben,1.0
wenn jedes Zwischenergebnis gerundet wird. Es andersrum tun, Sie Zwischenergebnisse erhalten0.2
,0.3
...,1.0
und schließlich2.0
. Dies ist ein extremes Beispiel, aber bei realistischen Gleitkommazahlen treten ähnliche Probleme auf. Die Grundidee ist, dass das Hinzufügen von Zahlen ähnlicher Größe zum kleinsten Fehler führt. Beginnen Sie mit den kleinsten Zahlen, da deren Summe größer ist und sich daher besser für die Addition zu größeren eignet.Ich glaube nicht, dass irgendetwas auf Sprachniveau getan werden kann oder sollte.
quelle
Decimal
beim Gleichheitstest. Der Unterschied zwischen1.0m/7.0m*7.0m
und1.0m
kann um viele Größenordnungen geringer sein als der Unterschied zwischen1.0/7.0*7.0
, aber er ist nicht Null.Standardmäßig sollten Sprachen Rationen mit beliebiger Genauigkeit für nicht ganzzahlige Zahlen verwenden.
Diejenigen, die optimieren müssen, können immer nach Schwimmern fragen. In C und anderen Programmiersprachen als Standardeinstellung sinnvoll, in den meisten heute verbreiteten Sprachen jedoch nicht.
quelle
double
. Wenn eine Berechnung auf einen Teil pro Million genau sein muss, ist es besser, eine Mikrosekunde damit zu verbringen, sie auf wenige Teile pro Milliarde genau zu berechnen, als eine Sekunde damit, sie absolut genau zu berechnen.Die zwei größten Probleme mit Gleitkommazahlen sind:
Der erste Fehlertyp kann nur behoben werden, indem ein zusammengesetzter Typ angegeben wird, der Wert- und Einheiteninformationen enthält. Zum Beispiel ein
length
oder einarea
Wert, der die Einheit enthält (Meter oder Quadratmeter oder Fuß bzw. Quadratfuß). Andernfalls müssen Sie fleißig damit umgehen, immer mit einer Maßeinheit zu arbeiten und nur dann in eine andere umzurechnen, wenn wir die Antwort mit einem Menschen teilen.Der zweite Fehlertyp ist ein konzeptioneller Fehler. Die Misserfolge manifestieren sich, wenn die Menschen sie als absolute Zahlen betrachten. Dies wirkt sich auf Gleichheitsoperationen, kumulative Rundungsfehler usw. aus. Beispielsweise kann es richtig sein, dass für ein System zwei Messungen innerhalb einer bestimmten Fehlergrenze äquivalent sind. Dh .999 und 1.001 sind ungefähr gleich wie 1.0, wenn Sie sich nicht für Unterschiede interessieren, die kleiner als +/- .1 sind. Allerdings sind nicht alle Systeme so nachsichtig.
Wenn eine Sprachniveauregelung benötigt wird, würde ich das Gleichheitspräzision nennen . In NUnit, JUnit und ähnlich aufgebauten Test-Frameworks können Sie die Genauigkeit steuern, die als korrekt angesehen wird. Beispielsweise:
Wenn beispielsweise C # oder Java geändert wurden, um einen Präzisionsoperator einzuschließen, könnte dies ungefähr so aussehen:
Wenn Sie jedoch eine solche Funktion bereitstellen, müssen Sie auch den Fall berücksichtigen, in dem die Gleichheit gut ist, wenn die +/- Seiten nicht gleich sind. Zum Beispiel würde + 1 / -10 zwei Zahlen als äquivalent betrachten, wenn eine von ihnen innerhalb von 1 mehr oder 10 weniger als die erste Zahl wäre. Um diesen Fall zu behandeln, müssen Sie möglicherweise auch ein
range
Schlüsselwort hinzufügen :quelle
Was können Programmiersprachen? Ich weiß nicht, ob es eine Antwort auf diese Frage gibt, da alles, was der Compiler / Interpreter im Auftrag des Programmierers unternimmt, um sein / ihr Leben zu erleichtern, normalerweise der Leistung, der Klarheit und der Lesbarkeit zuwiderläuft. Ich denke, sowohl der C ++ - Weg (zahle nur für das, was du brauchst) als auch der Perl-Weg (Prinzip der geringsten Überraschung) sind gültig, aber es hängt von der Anwendung ab.
Programmierer müssen immer noch mit der Sprache arbeiten und verstehen, wie sie mit Gleitkommawerten umgeht. Wenn dies nicht der Fall ist, werden sie Annahmen treffen, und eines Tages wird das verfolgte Verhalten nicht mit ihren Annahmen übereinstimmen.
Meine Einstellung zu dem, was der Programmierer wissen muss:
quelle
Verwenden Sie sinnvolle Standardeinstellungen, z. B. integrierte Unterstützung für Decmials.
Groovy macht das ganz gut, obwohl Sie mit ein wenig Aufwand immer noch Code schreiben können, um die Gleitkomma-Ungenauigkeit einzuführen.
quelle
Ich bin damit einverstanden, dass es auf sprachlicher Ebene nichts zu tun gibt. Programmierer müssen verstehen, dass Computer diskret und begrenzt sind und dass viele der in ihnen dargestellten mathematischen Konzepte nur Näherungswerte sind.
Egal Fließkomma. Man muss verstehen, dass die Hälfte der Bitmuster für negative Zahlen verwendet wird und dass 2 ^ 64 tatsächlich ziemlich klein ist, um typische Probleme mit der Ganzzahlarithmetik zu vermeiden.
quelle
x
==y
nicht impliziert, dass das Durchführen einer Berechnung fürx
dasselbe Ergebnis wie das Durchführen derselben Berechnung für ergibty
.Eine Sache, die Sprachen tun könnten: Entfernen Sie den Gleichheitsvergleich von anderen Gleitkommatypen als einem direkten Vergleich mit den NAN-Werten.
Gleichheitstests würden nur als Funktionsaufruf existieren, der die beiden Werte und ein Delta angenommen hat, oder für Sprachen wie C #, die es zulassen, dass Typen Methoden haben, die den anderen Wert und das Delta annehmen.
quelle
Ich finde es seltsam, dass niemand auf den rationalen Zahlentrick der Familie Lisp hingewiesen hat.
Im Ernst, öffne sbcl und mache das:
(+ 1 3)
und du bekommst 4. Wenn du das tust*( 3 2)
, bekommst du 6. Jetzt versuche(/ 5 3)
und du bekommst 5/3 oder 5 Drittel.Das sollte in manchen Situationen etwas helfen, oder?
quelle
Eine Sache, die ich gerne sehen würde, wäre die Erkenntnis, dass
double
tofloat
als eine zunehmende Konversion angesehen werden sollte, währendfloat
todouble
sich verengt (*). Das mag kontraintuitiv erscheinen, aber überlegen Sie, was die Typen tatsächlich bedeuten:Wenn man ein hat,
double
das die beste Darstellung der Größe "ein Zehntel" enthält und infloat
umrechnet, ist das Ergebnis "13.421.773,5 / 134.217.728, plus oder minus 1 / 268.435.456 oder so", was eine korrekte Beschreibung des Wertes ist.Im Gegensatz
float
dazudouble
ist das Ergebnis "13.421.773,5 / 134.217.728, plus oder minus 1 / 72.057.594.037.927.936 oder so" - ein Grad an impliziter Genauigkeit , wenn a die beste Darstellung der Menge "ein Zehntel" enthält und in diese umwandelt das ist falsch um einen Faktor von über 53 Millionen.Obwohl der IEEE-744-Standard verlangt, dass Gleitkomma-Berechnungen so durchgeführt werden, als ob jede Gleitkommazahl die exakte numerische Größe genau in der Mitte ihres Bereichs darstellt, sollte dies nicht bedeuten, dass die Gleitkomma-Werte tatsächlich diese exakten Werte darstellen numerische Größen. Das Erfordernis, dass die Werte in der Mitte ihrer Bereiche angenommen werden, ergibt sich vielmehr aus drei Tatsachen: (1) Berechnungen müssen so durchgeführt werden, als ob die Operanden einige bestimmte genaue Werte haben; (2) konsistente und dokumentierte Annahmen sind hilfreicher als inkonsistente oder undokumentierte Annahmen; (3) Wenn man eine konsistente Annahme machen will, kann keine andere konsistente Annahme besser sein als die Annahme, dass eine Menge das Zentrum ihres Bereichs darstellt.
Ich erinnere mich übrigens an ungefähr 25 Jahre zuvor, als jemand ein numerisches Paket für C erfand, das "Range Types" verwendete, die jeweils aus einem Paar 128-Bit-Floats bestanden. Alle Berechnungen würden so durchgeführt, dass für jedes Ergebnis der minimal und maximal mögliche Wert berechnet wird. Wenn man eine große lange iterative Berechnung durchführt und einen Wert von [12.53401391134 12.53902812673] feststellt, kann man sicher sein, dass viele Stellen der Genauigkeit durch Rundungsfehler verloren gegangen sind, das Ergebnis aber immer noch vernünftigerweise als 12,54 ausgedrückt werden kann (und es war nicht ' t wirklich 12,9 oder 53,2). Ich bin überrascht, dass ich keine Unterstützung für solche Typen in gängigen Sprachen gesehen habe, zumal sie gut zu mathematischen Einheiten passen, die mit mehreren Werten gleichzeitig arbeiten können.
(*) In der Praxis ist es oft hilfreich, Werte mit doppelter Genauigkeit zu verwenden, um Zwischenberechnungen durchzuführen, wenn mit Zahlen mit einfacher Genauigkeit gearbeitet wird. Daher kann es ärgerlich sein, für alle derartigen Vorgänge einen Typecast zu verwenden. Sprachen könnten helfen, indem sie einen "Fuzzy Double" -Typ haben, der Berechnungen als Double ausführt und frei von und nach Single umgewandelt werden kann. Dies wäre besonders hilfreich, wenn Funktionen, die Parameter vom Typ
double
und vom Rückgabewert annehmendouble
, markiert werden könnten, damit sie automatisch eine Überladung erzeugen, die stattdessen "Fuzzy Double" akzeptiert und zurückgibt.quelle
Wenn mehr Programmiersprachen eine Seite aus Datenbanken entnehmen und es den Entwicklern ermöglichen würden, die Länge und Genauigkeit ihrer numerischen Datentypen festzulegen, könnten sie die Wahrscheinlichkeit von Gleitkommafehlern erheblich verringern. Wenn eine Sprache es einem Entwickler ermöglicht, eine Variable als Gleitkomma (2) zu deklarieren, was darauf hinweist, dass eine Gleitkommazahl mit zwei Dezimalstellen Genauigkeit erforderlich ist, können mathematische Operationen viel sicherer ausgeführt werden. In diesem Fall wird die Variable intern als Ganzzahl dargestellt und vor dem Anzeigen des Werts durch 100 dividiert. In diesem Fall kann die Geschwindigkeit durch Verwendung der schnelleren Ganzzahl-Arithmetikpfade verbessert werden. Die Semantik eines Floats (2) würde es Entwicklern auch ermöglichen, die ständige Notwendigkeit zu vermeiden, Daten vor der Ausgabe zu runden, da ein Float (2) Daten inhärent auf zwei Dezimalstellen rundet.
Natürlich müssen Sie einem Entwickler erlauben, nach einem Gleitkommawert mit maximaler Genauigkeit zu fragen, wenn der Entwickler diese Genauigkeit benötigt. Und Sie würden Probleme einführen, bei denen geringfügig unterschiedliche Ausdrücke derselben mathematischen Operation aufgrund von Zwischenrundungsoperationen möglicherweise unterschiedliche Ergebnisse liefern, wenn Entwickler ihre Variablen nicht präzise genug ausführen. Aber zumindest in der Datenbankwelt scheint das keine allzu große Sache zu sein. Die meisten Menschen führen nicht die Art von wissenschaftlichen Berechnungen durch, die eine hohe Genauigkeit der Zwischenergebnisse erfordern.
quelle
Float(2)
wie du vorschlägst sollte nicht genannt werdenFloat
, da hier nichts schwebt, schon gar nicht der "Dezimalpunkt".Dies gilt in einigen Fällen, ist jedoch keine allgemeine Lösung für den Umgang mit Float-Werten. Die wirkliche Lösung besteht darin, das Problem zu verstehen und zu lernen, wie man damit umgeht. Wenn Sie Gleitkommaberechnungen verwenden, sollten Sie immer überprüfen, ob Ihre Algorithmen numerisch stabil sind . Es gibt ein riesiges Gebiet der Mathematik / Informatik, das sich auf das Problem bezieht. Es heißt Numerische Analyse .
quelle
Wie andere Antworten festgestellt haben, besteht die einzige Möglichkeit, Fließkommafälle in Finanzsoftware zu vermeiden, darin, sie dort nicht zu verwenden. Dies kann tatsächlich möglich sein - wenn Sie eine gut konzipierte Bibliothek für Finanzmathematik bereitstellen .
Funktionen zum Importieren von Gleitkommaschätzwerten sollten eindeutig als solche gekennzeichnet und mit für diese Operation geeigneten Parametern versehen sein, z.
Der einzige wirkliche Weg, um Fließkomma-Fallstricke im Allgemeinen zu vermeiden, ist die Bildung - Programmierer müssen etwas lesen und verstehen, was jeder Programmierer über Fließkomma-Arithmetik wissen sollte .
Ein paar Dinge, die helfen könnten:
isNear()
Funktion.quelle
Die meisten Programmierer wären überrascht, dass COBOL das richtig verstanden hat ... In der ersten Version von COBOL gab es keinen Fließkomma, nur Dezimalzahlen, und die Tradition in COBOL setzte sich bis heute fort, dass das erste, woran Sie denken, wenn Sie eine Zahl deklarieren, Dezimalzahlen sind. .. Gleitkomma würde nur verwendet, wenn Sie es wirklich brauchten. Als C kam, gab es aus irgendeinem Grund keinen primitiven Dezimaltyp. Meiner Meinung nach begannen hier alle Probleme.
quelle