TL; DR
- Verwenden Sie die folgende Funktion anstelle der derzeit akzeptierten Lösung, um in bestimmten Grenzfällen unerwünschte Ergebnisse zu vermeiden und gleichzeitig die Effizienz zu steigern.
- Kennen Sie die erwartete Ungenauigkeit Ihrer Zahlen und geben Sie sie in der Vergleichsfunktion entsprechend ein.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
return diff < std::max(relth, epsilon * norm);
}
Grafik bitte?
Beim Vergleich von Gleitkommazahlen gibt es zwei "Modi".
Der erste ist der relative Modus, bei dem der Unterschied zwischen x
und y
relativ zu ihrer Amplitude betrachtet wird |x| + |y|
. Beim Zeichnen in 2D wird das folgende Profil angezeigt, wobei Grün Gleichheit von x
und bedeutet y
. (Ich habe epsilon
zu Illustrationszwecken eine von 0,5 genommen).
Der relative Modus wird für "normale" oder "ausreichend große" Gleitkommawerte verwendet. (Dazu später mehr).
Der zweite ist ein absoluter Modus, wenn wir einfach ihre Differenz mit einer festen Zahl vergleichen. Es gibt das folgende Profil (wieder mit einem epsilon
von 0,5 und einem relth
von 1 zur Veranschaulichung).
Dieser absolute Vergleichsmodus wird für "winzige" Gleitkommawerte verwendet.
Die Frage ist nun, wie wir diese beiden Antwortmuster zusammenfügen.
In Michael Borgwardts Antwort basiert der Schalter auf dem Wert von diff
, der unten liegen sollte relth
( Float.MIN_NORMAL
in seiner Antwort). Diese Schaltzone ist in der folgenden Grafik schraffiert dargestellt.
Weil relth * epsilon
kleiner ist, dass relth
die grünen Flecken nicht zusammenkleben, was wiederum der Lösung eine schlechte Eigenschaft gibt: Wir können Drillinge von Zahlen finden, die x < y_1 < y_2
und doch x == y2
aber x != y1
.
Nehmen Sie dieses bemerkenswerte Beispiel:
x = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32
Wir haben x < y1 < y2
und ist in der Tat y2 - x
mehr als 2000-mal größer als y1 - x
. Und doch mit der aktuellen Lösung,
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
Im Gegensatz dazu basiert in der oben vorgeschlagenen Lösung die Schaltzone auf dem Wert von |x| + |y|
, der durch das schraffierte Quadrat unten dargestellt wird. Es stellt sicher, dass beide Zonen ordnungsgemäß verbunden sind.
Der obige Code hat auch keine Verzweigung, was effizienter sein könnte. Bedenken Sie, dass Vorgänge wie max
und abs
, die a priori verzweigt werden müssen, häufig dedizierte Montageanweisungen haben. Aus diesem Grund denke ich, dass dieser Ansatz einer anderen Lösung überlegen ist, die darin besteht, Michaels nearlyEqual
durch Ändern des Schalters von diff < relth
auf zu reparieren diff < eps * relth
, was dann im Wesentlichen das gleiche Antwortmuster erzeugen würde.
Wo kann man zwischen relativem und absolutem Vergleich wechseln?
Der Wechsel zwischen diesen Modi relth
erfolgt wie FLT_MIN
in der akzeptierten Antwort. Diese Wahl bedeutet, dass die Darstellung von float32
die Genauigkeit unserer Gleitkommazahlen einschränkt.
Das macht nicht immer Sinn. Wenn die Zahlen, die Sie vergleichen, beispielsweise das Ergebnis einer Subtraktion sind, ist möglicherweise etwas im Bereich von FLT_EPSILON
sinnvoller. Wenn es sich um Quadratwurzeln subtrahierter Zahlen handelt, kann die numerische Ungenauigkeit sogar noch höher sein.
Es ist ziemlich offensichtlich, wenn Sie einen Gleitkomma mit vergleichen 0
. Hier wird jeder relative Vergleich scheitern, weil |x - 0| / (|x| + 0) = 1
. Der Vergleich muss also in den absoluten Modus wechseln, wenn er x
in der Größenordnung der Ungenauigkeit Ihrer Berechnung liegt - und selten ist er so niedrig wie FLT_MIN
.
Dies ist der Grund für die Einführung des relth
obigen Parameters.
Auch durch die nicht multipliziert relth
mit epsilon
der Interpretation dieses Parameters ist einfach und entsprechen dem Niveau der numerischen Präzision , dass wir auf diese Zahlen erwarten.
Mathematisches Grollen
(hier meistens zu meinem eigenen Vergnügen gehalten)
Generell gehe ich davon aus, dass ein gut erzogener Gleitkomma-Vergleichsoperator =~
einige grundlegende Eigenschaften haben sollte.
Folgendes ist ziemlich offensichtlich:
- Selbstgleichheit:
a =~ a
- Symmetrie:
a =~ b
impliziertb =~ a
- Invarianz durch Opposition:
a =~ b
impliziert-a =~ -b
(Wir haben a =~ b
und b =~ c
implizieren a =~ c
, =~
ist keine Äquivalenzbeziehung).
Ich würde die folgenden Eigenschaften hinzufügen, die spezifischer für Gleitkomma-Vergleiche sind
- wenn
a < b < c
, dann a =~ c
impliziert a =~ b
(engere Werte sollten auch gleich sein)
- wenn
a, b, m >= 0
dann a =~ b
impliziert a + m =~ b + m
(größere Werte mit der gleichen Differenz sollten auch gleich sein)
- wenn
0 <= λ < 1
dann a =~ b
impliziert λa =~ λb
(vielleicht weniger offensichtlich zu argumentieren).
Diese Eigenschaften schränken mögliche Gleichheitsfunktionen bereits stark ein. Die oben vorgeschlagene Funktion überprüft sie. Möglicherweise fehlen eine oder mehrere ansonsten offensichtliche Eigenschaften.
Wenn man sich =~
eine Familie von Gleichheitsbeziehungen =~[Ɛ,t]
vorstellt, die durch Ɛ
und parametrisiert sind relth
, könnte man auch hinzufügen
- wenn
Ɛ1 < Ɛ2
dann a =~[Ɛ1,t] b
impliziert a =~[Ɛ2,t] b
(Gleichheit für eine gegebene Toleranz impliziert Gleichheit bei einer höheren Toleranz)
- wenn
t1 < t2
dann a =~[Ɛ,t1] b
impliziert a =~[Ɛ,t2] b
(Gleichheit für eine gegebene Ungenauigkeit impliziert Gleichheit bei einer höheren Ungenauigkeit)
Die vorgeschlagene Lösung überprüft auch diese.