Warum der Logarithmus in PHP und Lua nicht monoton ist

Log is non-monotonic in PHP and Lua

Ich untersuche ein seltsames Verhalten bei der Berechnung von Logarithmen in PHP und Lua. Obwohl die Mathematik klar sagt, dass ein größerer Basiswert ein kleineres Ergebnis liefern sollte, kippt das Ergebnis in diesen Sprachen bei bestimmten Werten. Der Grund liegt in einer inkonsistenten Mischung aus verschiedenen Implementierungsmethoden für spezielle Basen wie 10 und 2, was zu einer Diskontinuität führt, die in anderen Sprachen wie Rust nicht auftritt.

IEEE-754 ist unglaublich robust, aber nachlässige Implementierungsentscheidungen hier und dort verunreinigen es so sehr, dass Menschen fast jeden Fehler auf die inhärente Ungenauigkeit zurückführen.
  1. ReactiveJelly

    Jeder weiß bereits, dass Gleitkommaoperationen ungenau sind, und darüber einen Blogartikel zu schreiben, wäre nicht besonders spaßig.

    Nun ja... Ja und nein. Und ich möchte an der Aussage „FP-Operationen sind ungenau“ etwas meckern, weil das manchmal wichtig ist.

    Es taucht auch in Lua auf – Lua verwendet für alles 64-Bit-Doppel-Gleitkommazahlen, _sogar für Array-Indizes_, weil sie 53 Bits Mantisse haben und garantiert alle 32-Bit-Ganzzahlen mit 100%iger Genauigkeit darstellen können.

    Ich habe gerade Lua gestartet und `2 ^ 32 == 4294967296.0` sowie `2 ^ 32 + 1 == 4294967297.0` erhalten. Man kann 4 Milliarden Dinge in einer Lua-Tabelle speichern und darauf mit Doppel-Gleitkomma-Indizes zugreifen.

    Das übliche „0.1 + 0.2 != 0.3“ ist _keine_ Ungenauigkeit. Man könnte 1.000 Bits für eine Gleitkommazahl reservieren und trotzdem ein Beispiel für eine Zahl finden, die im Dezimalsystem trivial darstellbar ist, aber im Binärsystem unendlich oft wiederholt.

    Da ich schon dabei bin: Festkomma- und Gleitkommaarithmetik sind nicht magisch unterschiedlich. Gleitkommazahlen funktionieren bei sehr großen Werten besser. Festkommazahlen sind vorhersehbarer, laufen aber beim Quadrieren von Zahlen schnell aus dem Wertebereich. Das spielt in der 3D-Mathematik eine Rolle, wenn man die Länge von Vektoren berechnet oder Vektoren normalisiert.

    Die PlayStation 1 hatte keine zitternden Vertices und verzerrten Texturen wegen der Festkommaarithmetik. Sie hatte zitternde Vertices, weil ihre GPU keine subpixelgenaue Rendering-Fähigkeit hatte und nur 16 Bits Präzision besaß. Sie hatte verzerrte Texturen wegen der affinen Textur-Mapping-Methode. Die GPU der N64 hatte mehr Bits Präzision und sie verfügte über perspektivkorrektes Textur-Mapping. Es gibt kein 16-Bit-Gleitkommaformat, das die PS1 vor […] gerettet hätte.

  2. codeflo

    Es gibt ein weit verbreitetes Missverständnis (das vom Autor des Artikels nicht geteilt wird), dass Gleitkommaarithmetik „ungenau“ im Sinne sei, dass das Ergebnis um einen gewissen Betrag an zufälligem Rauschen abweicht. Im Gegenteil: IEEE-Gleitkommaergebnisse sind präzise so spezifiziert, dass sie den nächstliegenden darstellbaren Wert zum mathematisch exakten Ergebnis liefern.

    Aus Effizienzgründen garantieren Bibliotheksfunktionen (und manchmal leider auch Hardware-Implementierungen) dies nicht immer (d.h. den nächstliegenden darstellbaren Wert). Das ist in Ordnung, wenn die Funktion dann „approximate_inverse_square_root“ oder so genannt wird. Manchmal ist eine Abweichung um ein gewisses Epsilon ein guter Kompromiss für eine 10-fache Effizienzsteigerung. Ich bin unglücklich, wenn ein solcher Kompromiss in meine Mathematikbibliothek geschmuggelt wird, ohne dass ich gewarnt werde.

    Ich habe den Quellcode von Rust geprüft, um die Behauptung des Artikels zu bestätigen, dass es keine präzise log-Funktion hat, und tatsächlich, hier ist die Implementierung:

    pub fn log(self, base: f64) -> f64 {

    self.ln() / base.ln()

    }

    Ich bin sicher, dass andere Mathematikbibliotheken nicht besser sind. Aber das ist keine korrekte Implementierung eines Logarithmus mit beliebiger Basis. Eine solche Funktion sollte vielleicht gar nicht in der Standardbibliothek angeboten werden (da sie so trivial ist), oder zumindest nicht mit diesem Namen. Wenn ein Programmierer sich für einen schnellen, aber leicht falschen Wert entscheiden möchte, sollte er dies meiner Meinung nach explizit tun.

  3. lioeters

    Kürzlich las ich einen Artikel über den EML-Operator (exp-min-log), der Exponentiation und Logarithmus verwendet, um elementare mathematische Funktionen einschließlich arithmetischer Operationen zu erstellen. Es gab eine Tabelle mit Ergebnissen aus Tests über verschiedene Sprachen hinweg.

    Sprache Ergebnis von 2 x 3 Fehler

    ---

    Node.js v25.3.0 6.000000000000000 0

    Python 3.9.6 6.000000000000001 8.88e-16

    PHP 8.5.1 6.000000000000001 8.88e-16

    Go 1.26.2 6.000000000000000 0

    Rust 6.000000000000001 8.88e-16

    Es wurde spekuliert, dass dieser winzige Fehlerbereich von 1 ULP (unit in the last place) wahrscheinlich auf den Unterschied in der Implementierung von log() in den Sprachen zurückzuführen ist. Angeblich verwenden Python, PHP und Rust LLVMs libm (C-Mathematikbibliothek), aber vielleicht kompilieren Go und Node.js intern direkt zu CPU-Befehlen? Es wurde kein Beweis dafür vorgelegt, also war ich skeptisch gegenüber dieser Erklärung.

  4. kstrauser

    Ich war bereit zu lesen, wie die Konsolen-Logging-Funktion Schreibvorgänge so neu anordnen könnte, dass

    log('a');

    log('b');

    ergebnisweise

    b

    a

    ergeben würde oder so etwas, was nicht weniger überraschend gewesen wäre.

  5. cwt137

    Ich finde es interessant, dass der Blogautor PHP und Lua miteinander verknüpft hat. PHP verwendet einen JIT von Lua. Hängt das mit dem Log-Problem zusammen?

Mehr von diesem Tag

2026-07-29