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).FeqEqualdeckt 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:
FeqComparefolgt der IEEE-Semantik und liefert−1,0,1— oder2für „unvergleichbar“, sobald ein NaN beteiligt ist. Zwei NaN gelten dabei als gleich (0). Dieser Rückgabewert muss abgefangen werden; wer nur auf< 0prüft, behandelt „unvergleichbar„ wie „größer“.FeqTotalOrderlegt eine vollständige Ordnung über alle Bitmuster: hier ist NaN einsortierbar, das Ergebnis ist immer−1,0oder1. Das ist die Funktion zum Sortieren — ein Sortierverfahren mitFeqComparewü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.
FeqFloorDivist nicht/. Die rohe Ganzzahldivision schneidet zur Null hin ab:−7 / 2ergibt−3.FeqFloorDivrundet nach unten und liefert−4. Entsprechend hatFeqFloorModimmer das Vorzeichen des Divisors (1), der rohe Rest das des Dividenden (−1).
Das ist keine Feinheit: bei Ringpuffern, Gitterkoordinaten und Zeitfenstern istFloorModdie richtige Wahl, weil ein negativer Index sonst aus dem Puffer zeigt.−7 % 2 = −1als 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.
