Float Exposed
(float.exposed)- Dieser Artikel erklärt, wie Gleitkommawerte (float) im Speicher gespeichert und dargestellt werden.
- Der Fokus liegt auf der Umwandlung ihrer hexadezimalen und dezimalen Form in den tatsächlichen numerischen Wert.
- Er erläutert die Bereiche Sign, Exponent und Significand sowie ihre jeweilige Funktion.
- Enthalten sind Beispiele dafür, wie man interpretiert, welchen exakten binären und dezimalen Wert ein bestimmter float-Wert darstellt.
- Außerdem wird die Berechnung der Differenz (Delta) zwischen darstellbaren Werten angesprochen.
Analyse der Speicherstruktur von Gleitkommawerten
- Es gibt verschiedene Gleitkommaformate wie "halfb float float double".
- Jeder Wert kann als im Speicher abgelegter Wert in Form von Raw Hexadecimal Integer Value (hexadezimaler Ganzzahlwert) und Raw Decimal Integer Value (dezimaler Ganzzahlwert) betrachtet werden.
- Hexadezimale Daten werden in der Hexadecimal Form ("%a") angezeigt und mit der tatsächlichen Gleitkommadarstellung verknüpft.
- Die Position jedes Werts wird als Significand–Exponent Range dargestellt.
So werden Binär- und Dezimalwerte interpretiert
- Eine Gleitkommazahl kann in Base-2 (binärer Auswertungsformel) wie folgt dargestellt werden:
- (−12)02×102(100010012 − 011111112)×1.011111110010100000000002
→ Dies ist die numerische Auswertung über einen Binärausdruck.
- (−12)02×102(100010012 − 011111112)×1.011111110010100000000002
- In Base-10 (dezimaler Auswertungsformel) sieht das so aus:
- 1×210×1.4967041015625
→ Ausgedrückt als Produkt aus 2 hoch 10 und dem Nachkommateil.
- 1×210×1.4967041015625
- Auch der exakte Dezimalwert bei der tatsächlichen Umrechnung wird angezeigt:
- dargestellt etwa als 1.532625×103
Berechnung des Abstands zu benachbarten Werten (Delta)
- Das Delta (der Abstand) zwischen darstellbaren Werten ist von wichtiger Bedeutung.
- Jeweils angegeben wird der Abstand zum nächsten (Next) bzw. vorherigen (Previous) darstellbaren Wert (Delta to Next/Previous Representable Value).
- Beispiel: ±1.220703125×10-4
- Dieser Abstand hängt mit den signifikanten Stellen bzw. der Präzision eines Gleitkommawerts zusammen.
Zusammenfassung
- Die Speicherdarstellung von Gleitkommazahlen sowie die Prinzipien ihrer Umwandlung in Binär- und Dezimalform
- Erklärung der Struktur aus sign, exponent, significand
- Zusätzlich eine Übersicht über den Darstellungsbereich und die Abstände zu benachbarten Werten
1 Kommentare
Hacker-News-Kommentare
Zu diesem Thema ist diese Erklärung die beste: https://fabiensanglard.net/floating_point_visually_explained/ Als ich anfing, Hacker News zu lesen, bin ich auf diesen Beitrag gestoßen, und er hat mich motiviert, dabeizubleiben, weil solche Inhalte auf der Plattform weiterbestehen: https://news.ycombinator.com/item?id=29368529
Mir ist klar, dass ich vielleicht zu sehr in Richtung Mathematik denke, aber diese Erklärung war nicht gerade leicht verständlich Wenn man eine wirklich einfache Erklärung für Gleitkommazahlen will: Sie liefern ungefähr dieselbe Anzahl an Bits Präzision, unabhängig von der Größenordnung Das heißt, ob eine Zahl viel kleiner als 1 ist, in der Nähe von 1 liegt oder sehr groß ist, man kann in den führenden Bits fast dieselbe Genauigkeit erwarten Das ist die Kerneigenschaft, aber sie wirklich zu verinnerlichen ist nicht einfach
Passt auch sehr gut zum Kontext des Blogs, den das TM-Forschungsteam kürzlich geschrieben hat https://news.ycombinator.com/item?id=45200925
Ich habe das noch nie so gut erklärt gesehen, also danke fürs Teilen
Eines der Probleme, über das ich lange nachgedacht habe, ist: „Wie stellt man einen
float-Wert als möglichst kurzen und dennoch eindeutigen Dezimal-String dar?“ Wenn man zum Beispiel Single-Precision-floatverwendet, braucht man bis zu 9 Dezimalstellen Präzision, um einenfloateindeutig zu identifizieren Deshalb muss man einprintf-Muster wie%.9gverwenden Dann wird allerdings0.1als unschöner Wert wie0.100000001ausgegeben Deshalb rundet man zur Darstellung oft auf 6 Stellen, und mit%.6gkönnen bis zu 6-stellige eingegebene Dezimalwerte identisch zum gespeicherten Wert ausgegeben werden Für Werte, die aus Berechnungen stammen, ist das Round-Trip-Verhalten dann aber nicht mehr sicher Das ist besonders wichtig, wenn manfloat-Werte exakt vergleichen muss, zum Beispiel um festzustellen, ob sich Daten geändert haben Meine Idee war, zuerst mit 6 Stellen auszugeben und, wenn beim Parsen wieder derselbe Binärwert herauskommt, das zu verwenden; andernfalls mit 7, 8 und 9 Stellen weiterzumachen, bis die kürzeste Dezimaldarstellung gefunden ist Mein Algorithmus sieht so ausIch frage mich, ob es eine effizientere Methode gibt, die kürzeste Darstellung zu finden, ohne
printf/scanfzu wiederholenDieses Problem ist tatsächlich wichtig Man kann es als das Problem auffassen, aus einem bestimmten
floateinen „kanonischen“ String zu machen Deshalb gibt es verschiedene effiziente Algorithmen wie Dragon4, Grisu3, Ryu und Dragonbox Googles Bibliothek double-conversion implementiert die ersten beidenEs gibt eine bessere Methode ohne
printf/scanf-Schleife Schonprintf("%f", ...)allein kann das nicht leisten Der eigentliche Algorithmus für die Umwandlung vonfloatnach String ist ziemlich komplex Ein guter neuerer Algorithmus ist https://github.com/ulfjack/ryu Soweit ich weiß, ist vor Kurzem noch etwas Effizienteres erschienen, aber ich erinnere mich nicht an den NamenMan sollte sich nicht zu sehr von negativen Reaktionen beeindrucken lassen; auch wenn es nicht die beste Methode ist, funktioniert sie normalerweise gut genug, solange keine Fehler auftreten Ich hatte tatsächlich einmal ein ähnliches Erlebnis: Ich wollte einen Vektor finden, der nach einer Euler-Rotation
(5°, 5°, 0)wieder derselbe Vektor wäre, also habe ich zufällig Vektoren leicht verschoben und geprüft, ob sie dem Referenzvektor näher kommen Ich habe die Schleife Millionen Male ausgeführt und in Python innerhalb weniger Sekunden ein Ergebnis bekommen Für eine Bibliotheksebene wäre das ineffizient, aber für meinen Anwendungsfall war ich sehr zufriedenstd::numeric_limits<float>::max_digits10ist dazu nützlich https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10.htmlSinnlos, und man sollte
sscanf()absolut nie verwenden Wenn man in einen vorzeichenlosen Integer umwandelt und so serialisiert/wiederherstellt, ist das verlustfrei reversibelWenn man eine kürzere Darstellung braucht, kann man eine Heuristik verwenden, die eine vollständige Wiederherstellung erlaubt, solange die ursprüngliche Genauigkeit garantiert bleibt, zum Beispiel über Idempotenz
Mein liebster FP-Tipp ist, dass sich
float-Vergleiche fast wie Integer-Vergleiche verwenden lassen Uma > bzu prüfen, kann manaundbals vorzeichenbehaftete Integer interpretieren und einfach vergleichen Das funktioniert (fast) gut Der nächstgrößerefloat-Wert ist also einfach das Bitmuster als Integer plus 1 Wenn man zum Beispiel bei0.0alsfloatanfängt und per Integer-Addition 1 addiert, erhält man direkt den nächstenfloat-Wert (denormal, die winzige Lücke ganz unten) So wird auchnextafterimplementiert Wenn man weiß, dass die Ordnung vonfloat-Werten der Integer-Vergleichsordnung entspricht, fühlt sich das viel natürlicher an Natürlich gibt es Ausnahmen:NaN, Unendlichkeit, negatives Null usw. sind anders Es gibt ein paar nützliche Anwendungen dafür, aber nicht für allesSo stimmt das nicht ganz Für positive Zahlen oder Vergleiche zwischen positiv und negativ stimmt es, aber bei zwei negativen Zahlen nicht Standard-Gleitkommazahlen (
float) verwenden Sign-Magnitude, moderne vorzeichenbehaftete Integer Zweierkomplement Bei negativen Zahlen ist die Vergleichsrichtung zwischen beiden umgekehrt Wenn man einenfloatwie einenintinkrementiert, bewegt man sich normalerweise innerhalb desselben Vorzeichens zu einem betragsmäßig größeren Wert Positive Zahlen gehen also nach oben, negative in Richtung kleinerer negativer Werte nach unten Bei Integern geht es immer nach oben oder in einen Overflow Präziser wäre zu sagen, dass es der Vergleichsordnung von Sign-Magnitude-Integern entspricht Die genannten Einschränkungen gelten natürlich weiterhinZur Referenz: Der total-order-Gleitkomma-Vergleichsalgorithmus der Rust-Standardbibliothek, bei dem auch
NaNvergleichbar ist, sieht so aus (IEEE 751 empfohlen)Den vollständigen Algorithmus ansehen
Diesem Thema bin ich in meinem OMSCS-Kurs zu Game AI begegnet, in dem es um Vorsichtsmaßnahmen bei der Darstellung von Positionen von Spielobjekten mit Gleitkommazahlen ging Es ist riskant, weil
floatmit zunehmender Entfernung vom Ursprung oder Referenzpunkt größere Werte speichern muss und dadurch Präzision verliertInteressant ist, dass sich dieses Phänomen im Minecraft-Mythos der Far Lands niedergeschlagen hat Je weiter man sich also vom Weltursprung entfernt, desto seltsamer werden Geländegenerierung oder Physik, und noch viel weiter draußen bricht alles komplett zusammen Das hat fast etwas Okkultes, als würden die Gesetze der Realität nach und nach zerfallen All das liegt an den Präzisionsgrenzen von
floatWenn man bei
floatviele Zahlen zwischen 0 und 1 addiert, ist es deutlich genauer, sie paarweise zu summieren und diese Summen dann weiter zusammenzuführen, als sie einfach der Reihe nach aufzuaddieren Das ist ein Beispiel dafür, wie gravierend sich kumulativefloat-Fehler auswirken können Tatsächlich gab es Fälle, in denen solche Gleitkommafehler ignoriert wurden und Probleme verursachten Donald Knuth erklärt in „The Art of Computer Programming“ solche grundlegenden Wahrheiten überfloat, etwa dassa + (b + c) ≠ (a + b) + cAuch in der Praxis gab es Vorfälle: Beim Patriot-Raketensystem wurde die Zeitakkumulation mitfloatverarbeitet, wodurch sich der Fehler immer weiter aufaddierte, bis das System das Ziel komplett verfehlte und neu gestartet werden musste Es musste alle 24 Stunden neu gebootet werden, und schließlich wurde die Systemsoftware nachgebessert Es gab auch Fälle, in denen große Strukturen einstürzten, weil ein Dickenwert durch Gleitkommafehler zu klein berechnet wurdeMan sollte zuerst die Randbedingungen definieren und daraus ableiten, welche Präzision benötigt wird Dann kann man auch Mindest- und Höchstdistanzen im Voraus berechnen Wenn die Welt zu groß wird, muss man sie in Sektoren aufteilen oder globale und lokale Koordinaten getrennt verwalten, wie etwa bei No Man's Sky Spiele sind letztlich Bühnenzauber Double-Precision reicht für die meisten Situationen völlig aus Wichtig ist, sich zu merken, keine kleinen und großen Werte zusammen zu addieren
In Kerbal Space Program wurde eine Menge kluger Technik eingesetzt, um ein Sonnensystem nur mit 32-Bit-
floatumzusetzen Dazu gibt es viele empfehlenswerte Artikel und VideosDiese Visualisierung macht Spaß, und ich finde es interessant, dass sie visuell einem CIDR range calculator ähnelt, den ich früher gebaut habe, um Netzwerkbereiche verständlicher zu machen Solche Visualisierungen sind sehr nützlich
Früher habe ich zum Erkunden der
float-Darstellung https://www.h-schmidt.net/FloatConverter/IEEE754.html verwendet Ein Vorteil dieser Seite ist, dass sie auch Umwandlungsfehler zeigt, allerdings unterstützt sie keine Double-Precisionfloatohnehin gut verstehen, mag das selbstverständlich sein, aber wer es zum ersten Mal lernt, braucht an dieser Stelle zusätzliche ErklärungEs wurde in diesen Kommentaren noch nicht geteilt, aber meine Lieblingsseite zu
floatist https://0.30000000000000004.com/Bei 32-Bit-
floatist die „interessanteste Ganzzahl“ wohl16777217(bei 64 Bit9007199254740992) Solche Edge Cases beim Testen zu kennen macht SpaßFür 64-Bit-
floatist9007199254740991in JavaScriptNumber.MAX_SAFE_INTEGERDieser Wert ist ungerade, und auch der nächste Wert9007199254740992ist für sich genommen noch sicher, aber ein klar unsicherer Wert wie9007199254740993wird gerundet und ist dadurch nicht mehr unterscheidbarBei 64-Bit-
floatist es exakt±9,007,199,254,740,993.0:-) Zur Einordnung: Solche Werte meinen genau den ersten Wert nach der Grenze der größten Ganzzahl, diefloatnoch „exakt“ darstellen kann Bei 32-Bit-floatist zum Beispiel der nächste darstellbare Wert nach±16,777,216.0bereits±16,777,218.0±16,777,217.0ist nicht darstellbar und wird normalerweise in Richtung Null oder ähnlich gerundet Solche Präzisionsgrenzen und Rundungsprobleme werden oft übersehenIch bin froh, dass IEEE754 existiert, aber IEEE754 ist nicht perfekt, und ich halte Werte wie posit für besser, wenn man Hardware-Support außer Acht lässt Big-Num-Rationalzahlen sind beiden noch überlegen, aber auch am langsamsten
Es wäre wirklich großartig, wenn auch die verschiedenen fp8-Formate unterstützt würden, die kürzlich auf GPUs eingeführt wurden