std.feq — Fließkomma vergleichen und runden

Fließkommazahlen auf der Bit-Ebene: Vergleiche mit Toleranz, ULP-Abstand, Klassifikation, totale Ordnung und Rundungsarten, die der Bestand nicht hat.

Mathematik-Units · std.mathx · std.sum · Datentypen

import std.feq;

Schicht 0, hängt an std.math und std.alloc. 36 Funktionen. Alle Beispiele mit lyxc 1.0.21A übersetzt und ausgeführt.


Warum ''=='' bei f64 nicht reicht

unit main;
import std.io;
import std.string;
import std.feq;

fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn ZB(t: pchar, v: bool): void { Print(t); PrintLn(IntToStr(v as int64)); }

fn main(): int64 {
    var a: f64 := 0.1;
    var b: f64 := 0.2;
    var s: f64 := a + b;
    var d: f64 := 0.3;

    ZB("0.1+0.2 == 0.3        : ", s == d);
    ZI("UlpsBetween(s, 0.3)   : ", FeqUlpsBetween(s, d));
    ZB("FeqUlps(s,0.3,4,0.0)  : ", FeqUlps(s, d, 4, 0.0));
    ZB("FeqRel(s,0.3,1e-12)   : ", FeqRel(s, d, 0.000000000001));
    return 0;
}

0.1+0.2 == 0.3        : 0
UlpsBetween(s, 0.3)   : 1
FeqUlps(s,0.3,4,0.0)  : 1
FeqRel(s,0.3,1e-12)   : 1

0.1 + 0.2 ist nicht 0.3 — aber die beiden liegen genau ein ULP auseinander, also so nah wie zwei verschiedene f64-Zahlen überhaupt sein können. Genau das misst FeqUlpsBetween: nicht „wie groß ist der Unterschied„, sondern „wie viele darstellbare Zahlen liegen dazwischen“.

Vergleich Wann
FeqAbs(a, b, absTol) Werte in bekannter Größenordnung, Toleranz absolut
FeqRel(a, b, relTol) Werte über viele Größenordnungen, Toleranz relativ
FeqEqual(a, b, absTol, relTol) beides zugleich — absolut nahe null, sonst relativ
FeqUlps(a, b, maxUlps, absTol) Rechenfehler zählen statt schätzen
FeqZero(a, absTol) gegen null, wo relativ sinnlos ist
 
Die Wahl ist keine Geschmacksfrage. Relativer Vergleich versagt in der Nähe von null (die Differenz zweier winziger Zahlen ist relativ riesig), absoluter Vergleich versagt bei großen Zahlen (1e12 und 1e12+1 sind absolut weit auseinander, praktisch aber gleich). FeqEqual deckt beides ab und ist die richtige Vorgabe, wenn die Größenordnung unbekannt ist.

Die Bit-Ebene

unit main;
import std.io;
import std.string;
import std.feq;

fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }

fn main(): int64 {
    ZI("Bits(1.0)          : ", FeqBits(1.0));
    ZI("Bits(Epsilon)      : ", FeqBits(FeqEpsilon()));
    ZI("Bits(Ulp(1.0))     : ", FeqBits(FeqUlp(1.0)));
    ZI("Bits(MinSubnormal) : ", FeqBits(FeqMinSubnormal()));
    ZI("Ulps(1, NextUp(1)) : ", FeqUlpsBetween(1.0, FeqNextUp(1.0)));
    return 0;
}

Bits(1.0)          : 4607182418800017408
Bits(Epsilon)      : 4372995238176751616
Bits(Ulp(1.0))     : 4372995238176751616
Bits(MinSubnormal) : 1
Ulps(1, NextUp(1)) : 1

Epsilon und Ulp(1.0) sind dasselbe — das Epsilon ist der Abstand von 1 zur nächsten darstellbaren Zahl. MinSubnormal hat das Bitmuster 1: die kleinste von null verschiedene f64-Zahl überhaupt.

<WRAP alert> PrintF64 verdeckt jeden Genauigkeitsbefund. Die Ausgabe hat sechs Nachkommastellen — FeqEpsilon() erscheint darin als 0.000000. Wer Genauigkeit misst, gibt das Bitmuster aus (FeqBits) und wandelt außerhalb zurück. Ein Fuzz-Lauf über Atan2F64 meldete auf diesem Weg einmal „keine Abweichung„, während die Funktion sechs Stellen zu wenig hatte.

PrintF64 setzt außerdem selbst einen Zeilenumbruch. Ein nachgestelltes PrintLn(“„) erzeugt eine Leerzeile. </WRAP>

Funktion Liefert
FeqBits(x) / FeqFromBits(b) Bitmuster hin und zurück
FeqUlp(x) Abstand zur nächsten darstellbaren Zahl bei x
FeqNextUp(x) / FeqNextDown(x) Nachbarzahl nach oben / unten
FeqUlpsBetween(a, b) Anzahl darstellbarer Zahlen dazwischen
FeqEpsilon() · FeqMax() · FeqMinNormal() · FeqMinSubnormal() Eckwerte

Klassifikation und Ordnung

unit main;
import std.io;
import std.string;
import std.feq;

fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); }

fn main(): int64 {
    var nan: f64 := FeqFromBits(9221120237041090560);
    var inf: f64 := FeqFromBits(9218868437227405312);

    ZI("IsNaN(nan)          : ", FeqIsNaN(nan) as int64);
    ZI("IsInf(inf)          : ", FeqIsInf(inf) as int64);
    ZI("Compare(1.0, nan)   : ", FeqCompare(1.0, nan));
    ZI("TotalOrder(1.0, nan): ", FeqTotalOrder(1.0, nan));
    ZF("SafeDiv(1, 0, -1)   : ", FeqSafeDiv(1.0, 0.0, -1.0));
    return 0;
}

IsNaN(nan)          : 1
IsInf(inf)          : 1
Compare(1.0, nan)   : 2
TotalOrder(1.0, nan): -1
SafeDiv(1, 0, -1)   : -1.000000

Die beiden Vergleichsfunktionen haben verschiedene Aufgaben, und der Unterschied zeigt sich genau bei NaN:

  • FeqCompare folgt der IEEE-Semantik und liefert −1, 0, 1 — oder 2 für „unvergleichbar“, sobald ein NaN beteiligt ist. Zwei NaN gelten dabei als gleich (0). Dieser Rückgabewert muss abgefangen werden; wer nur auf < 0 prüft, behandelt „unvergleichbar„ wie „größer“.
  • FeqTotalOrder legt eine vollständige Ordnung über alle Bitmuster: hier ist NaN einsortierbar, das Ergebnis ist immer −1, 0 oder 1. Das ist die Funktion zum Sortieren — ein Sortierverfahren mit FeqCompare würde bei NaN in den Daten in eine unbestimmte Reihenfolge laufen.

FeqSafeDiv(a, b, fallback) gibt bei Division durch null den Ersatzwert zurück, statt inf oder NaN in die Rechnung zu tragen.

Dazu FeqIsFinite, FeqIsZero, FeqIsSubnormal, FeqSignBit, FeqClamp und FeqLerp.


Runden — sieben Arten, und der Bruch bei negativen Zahlen

unit main;
import std.io;
import std.string;
import std.feq;

fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); }
fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); }

fn main(): int64 {
    ZF("RoundHalfAway(2.5) : ", FeqRoundHalfAway(2.5));
    ZF("RoundHalfEven(2.5) : ", FeqRoundHalfEven(2.5));
    ZF("RoundHalfAway(3.5) : ", FeqRoundHalfAway(3.5));
    ZF("RoundHalfEven(3.5) : ", FeqRoundHalfEven(3.5));
    ZF("RoundTo(3.14159,2) : ", FeqRoundTo(3.14159, 2));

    ZI("FloorDiv(-7, 2)    : ", FeqFloorDiv(-7, 2));
    ZI("roh     -7 / 2     : ", -7 / 2);
    ZI("FloorMod(-7, 2)    : ", FeqFloorMod(-7, 2));
    ZI("roh     -7 % 2     : ", -7 % 2);
    ZI("CeilDiv(7, 2)      : ", FeqCeilDiv(7, 2));
    return 0;
}

RoundHalfAway(2.5) : 3.000000
RoundHalfEven(2.5) : 2.000000
RoundHalfAway(3.5) : 4.000000
RoundHalfEven(3.5) : 4.000000
RoundTo(3.14159,2) : 3.140000
FloorDiv(-7, 2)    : -4
roh     -7 / 2     : -3
FloorMod(-7, 2)    : 1
roh     -7 % 2     : -1
CeilDiv(7, 2)      : 4

Bei 2.5 gehen die beiden Rundungsarten auseinander, bei 3.5 nicht. HalfEven rundet zur geraden Nachbarzahl — 2.5 wird zu 2, 3.5 zu 4. Über viele Werte hebt sich der Rundungsfehler damit auf, statt sich zu summieren; deshalb ist es die Vorgabe in der Statistik und im Bankwesen. HalfAway ist die Schulregel und rundet 2.5 auf 3.

 
FeqFloorDiv ist nicht /. Die rohe Ganzzahldivision schneidet zur Null hin ab: −7 / 2 ergibt −3. FeqFloorDiv rundet nach unten und liefert −4. Entsprechend hat FeqFloorMod immer das Vorzeichen des Divisors (1), der rohe Rest das des Dividenden (−1).

Das ist keine Feinheit: bei Ringpuffern, Gitterkoordinaten und Zeitfenstern ist FloorMod die richtige Wahl, weil ein negativer Index sonst aus dem Puffer zeigt. −7 % 2 = −1 als Feldindex ist ein Zugriff daneben.

Dazu FeqTrunc, FeqFrac, FeqFloorTo und FeqCeilTo (Rundung auf ein beliebiges Raster, etwa Vielfache von 0,05).


Letzte Aktualisierung: 2026-08-16 · alle Beispiele mit lyxc 1.0.21A übersetzt und ausgeführt; Bitmuster, Rundungs- und Divisionswerte gegen eine unabhängige Referenzrechnung geprüft.