3 Punkte von GN⁺ 2025-09-13 | 1 Kommentare | Auf WhatsApp teilen
  • 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.
  • In Base-10 (dezimaler Auswertungsformel) sieht das so aus:
    • 1×​210×​1.4967041015625
      → Ausgedrückt als Produkt aus 2 hoch 10 und dem Nachkommateil.
  • 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

 
GN⁺ 2025-09-13
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-float verwendet, braucht man bis zu 9 Dezimalstellen Präzision, um einen float eindeutig zu identifizieren Deshalb muss man ein printf-Muster wie %.9g verwenden Dann wird allerdings 0.1 als unschöner Wert wie 0.100000001 ausgegeben Deshalb rundet man zur Darstellung oft auf 6 Stellen, und mit %.6g kö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 man float-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 aus

    int out_length;
    char buffer[32];
    for (int prec = 6; prec<=9; prec++) {
      out_length = sprintf(buffer, "%.*g", prec, floatValue);
      if (prec == 9) {
        break;
      }
      float checked_number;
      sscanf(buffer, "%g", &checked_number);
      if (checked_number == floatValue) {
        break;
      }
    }
    

    Ich frage mich, ob es eine effizientere Methode gibt, die kürzeste Darstellung zu finden, ohne printf/scanf zu wiederholen

    • Dieses Problem ist tatsächlich wichtig Man kann es als das Problem auffassen, aus einem bestimmten float einen „kanonischen“ String zu machen Deshalb gibt es verschiedene effiziente Algorithmen wie Dragon4, Grisu3, Ryu und Dragonbox Googles Bibliothek double-conversion implementiert die ersten beiden

    • Es gibt eine bessere Methode ohne printf/scanf-Schleife Schon printf("%f", ...) allein kann das nicht leisten Der eigentliche Algorithmus für die Umwandlung von float nach 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 Namen

    • Man 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 zufrieden

    • std::numeric_limits<float>::max_digits10 ist dazu nützlich https://en.cppreference.com/w/cpp/types/numeric_limits/max_digits10.html

    • Sinnlos, und man sollte sscanf() absolut nie verwenden Wenn man in einen vorzeichenlosen Integer umwandelt und so serialisiert/wiederherstellt, ist das verlustfrei reversibel

      double f = 0.0/0.0; // bei manchen Compilern ist eventuell ein Soft-Error-Flag nötig
      double g;
      char s[9];
      
      assert(sizeof double == sizeof uint64_t);
      
      snprintf(s, 9, "%0" PRIu64, *(uint64_t *)(&f));
      
      snscanf(s, 9, "%0" SCNu64, (uint64_t *)(&g));
      

      Wenn 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 Um a > b zu prüfen, kann man a und b als vorzeichenbehaftete Integer interpretieren und einfach vergleichen Das funktioniert (fast) gut Der nächstgrößere float-Wert ist also einfach das Bitmuster als Integer plus 1 Wenn man zum Beispiel bei 0.0 als float anfängt und per Integer-Addition 1 addiert, erhält man direkt den nächsten float-Wert (denormal, die winzige Lücke ganz unten) So wird auch nextafter implementiert Wenn man weiß, dass die Ordnung von float-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 alles

    • So 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 einen float wie einen int inkrementiert, 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 weiterhin

    • Zur Referenz: Der total-order-Gleitkomma-Vergleichsalgorithmus der Rust-Standardbibliothek, bei dem auch NaN vergleichbar ist, sieht so aus (IEEE 751 empfohlen)

      let mut left = self.to_bits() as i32;
      let mut right = other.to_bits() as i32;
      
      // Bei negativen Zahlen werden alle Bits außer dem Vorzeichen invertiert,
      // sodass sich eine Ordnung ergibt, die dem Vergleich von Zweierkomplement-Integern ähnelt
      
      left ^= (((left >> 31) as u32) >> 1) as i32;
      right ^= (((right >> 31) as u32) >> 1) as i32;
      
      left.cmp(&right)
      

      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 float mit zunehmender Entfernung vom Ursprung oder Referenzpunkt größere Werte speichern muss und dadurch Präzision verliert

    • Interessant 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 float

    • Wenn man bei float viele 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 kumulative float-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 über float, etwa dass a + (b + c) ≠ (a + b) + c Auch in der Praxis gab es Vorfälle: Beim Patriot-Raketensystem wurde die Zeitakkumulation mit float verarbeitet, 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 wurde

    • Man 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-float umzusetzen Dazu gibt es viele empfehlenswerte Artikel und Videos

  • Diese 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-Precision

    • Ich habe auch schon geschaut, ob das jemand erwähnt hat, und es ist wirklich eine großartige Webseite Die im OP vorgestellte Seite erklärt allerdings die Aufteilungsstruktur des Zahlenraums grafisch wirklich sehr intuitiv Die y-Achse ist logarithmisch skaliert, und die x-Achse ist pro Zeile zwar linear, aber auf die jeweiligen Log-Intervalle normalisiert Für Leute, die float ohnehin gut verstehen, mag das selbstverständlich sein, aber wer es zum ersten Mal lernt, braucht an dieser Stelle zusätzliche Erklärung
  • Es wurde in diesen Kommentaren noch nicht geteilt, aber meine Lieblingsseite zu float ist https://0.30000000000000004.com/

  • Bei 32-Bit-float ist die „interessanteste Ganzzahl“ wohl 16777217 (bei 64 Bit 9007199254740992) Solche Edge Cases beim Testen zu kennen macht Spaß

    • Für 64-Bit-float ist 9007199254740991 in JavaScript Number.MAX_SAFE_INTEGER Dieser Wert ist ungerade, und auch der nächste Wert 9007199254740992 ist für sich genommen noch sicher, aber ein klar unsicherer Wert wie 9007199254740993 wird gerundet und ist dadurch nicht mehr unterscheidbar

    • Bei 64-Bit-float ist 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, die float noch „exakt“ darstellen kann Bei 32-Bit-float ist zum Beispiel der nächste darstellbare Wert nach ±16,777,216.0 bereits ±16,777,218.0 ±16,777,217.0 ist nicht darstellbar und wird normalerweise in Richtung Null oder ähnlich gerundet Solche Präzisionsgrenzen und Rundungsprobleme werden oft übersehen

  • Ich 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

    • IEEE754 ist ein Kompromiss, der viele Anforderungen unter einen Hut bringt Manche Alternativen sind in bestimmten Bereichen überlegen, in anderen aber schlechter
  • Es wäre wirklich großartig, wenn auch die verschiedenen fp8-Formate unterstützt würden, die kürzlich auf GPUs eingeführt wurden