Ich habe versucht herauszufinden, wo float
die Fähigkeit verloren geht, große Ganzzahlen genau darzustellen. Also habe ich diesen kleinen Ausschnitt geschrieben:
int main() {
for (int i=0; ; i++) {
if ((float)i!=i) {
return i;
}
}
}
Dieser Code scheint mit allen Compilern außer clang zu funktionieren. Clang erzeugt eine einfache Endlosschleife. Godbolt .
Ist das erlaubt? Wenn ja, handelt es sich um ein QoI-Problem?
c++
floating-point
clang
geza
quelle
quelle
gcc
führt die gleiche Endlosschleifenoptimierung durch, wenn Sie-Ofast
stattdessen mit kompilieren , sodass eine Optimierunggcc
als unsicher eingestuft wird, dies jedoch möglich ist.ucomiss xmm0,xmm0
, um(float)i
mit sich selbst zu vergleichen . Das war Ihr erster Hinweis darauf, dass Ihre C ++ - Quelle nicht das bedeutet, was Sie dachten. Wollen Sie behaupten, Sie hätten diese Schleife zum Drucken / Zurückgeben16777216
? Mit welchem Compiler / Version / Optionen war das? Denn das wäre ein Compiler-Bug. gcc optimiert Ihren Code korrektjnp
als Schleifenzweig ( godbolt.org/z/XJYWeu ): Führen Sie eine Schleife durch, solange die Operanden!=
nicht NaN waren.-ffast-math
Option, die implizit aktiviert wird und-Ofast
es GCC ermöglicht, unsichere Gleitkommaoptimierungen anzuwenden und somit denselben Code wie Clang zu generieren. MSVC verhält sich genauso: Ohne/fp:fast
generiert es eine Menge Code, der zu einer Endlosschleife führt. mit/fp:fast
gibt es eine einzelnejmp
Anweisung aus. Ich gehe davon aus, dass diese Compiler, ohne unsichere FP-Optimierungen explizit zu aktivieren, an den IEEE 754-Anforderungen bezüglich NaN-Werten hängen bleiben. Ziemlich interessant, dass Clang das eigentlich nicht tut. Sein statischer Analysator ist besser. @ 12345ieee(float) i
vom mathematischen Wert von abweichti
, wäre das Ergebnis (der in derreturn
Anweisung zurückgegebene Wert ) 16.777.217 und nicht 16.777.216.Antworten:
Wie @Angew hervorhob , benötigt der
!=
Bediener auf beiden Seiten den gleichen Typ.(float)i != i
führt zu einer Förderung der RHS, um auch zu schweben, so haben wir(float)i != (float)i
.g ++ generiert auch eine Endlosschleife, optimiert jedoch nicht die Arbeit von innen heraus. Sie können sehen, dass es int-> float mit konvertiert
cvtsi2ss
und mit sich selbstucomiss xmm0,xmm0
vergleicht(float)i
. (Das war Ihr erster Hinweis darauf, dass Ihre C ++ - Quelle nicht das bedeutet, was Sie für die Antwort von @ Angew gehalten haben.)x != x
ist nur wahr, wenn es "ungeordnet" ist, weilx
NaN war. (INFINITY
Vergleiche in der IEEE-Mathematik gleich, aber NaN nicht.NAN == NAN
ist falsch,NAN != NAN
ist wahr).gcc7.4 und älter optimiert Ihren Code korrekt
jnp
als Schleifenzweig ( https://godbolt.org/z/fyOhW1 ): Führen Sie eine Schleife durch, solange die Operandenx != x
nicht NaN waren. (gcc8 und höher prüfen auch, obje
die Schleife unterbrochen wurde, und können nicht optimieren, da dies für alle Nicht-NaN-Eingaben immer der Fall ist.) x86 FP vergleicht den eingestellten PF mit ungeordnet.Übrigens, das bedeutet, dass die Optimierung von clang auch sicher ist : Es muss nur CSE
(float)i != (implicit conversion to float)i
als gleich sein und beweisen, dassi -> float
es für den möglichen Bereich von niemals NaN istint
.(Obwohl diese Schleife UB mit vorzeichenbehaftetem Überlauf trifft, darf sie buchstäblich jeden gewünschten Asm ausgeben, einschließlich einer
ud2
illegalen Anweisung oder einer leeren Endlosschleife, unabhängig davon, was der Schleifenkörper tatsächlich war.) Ignoriert jedoch den UB mit vorzeichenbehaftetem Überlauf ist diese Optimierung noch zu 100% legal.GCC kann den Schleifenkörper nicht optimieren, selbst wenn der
-fwrapv
Überlauf mit vorzeichenbehafteten Ganzzahlen genau definiert ist (als 2er-Komplement-Wraparound). https://godbolt.org/z/t9A8t_Auch das Aktivieren
-fno-trapping-math
hilft nicht. (Die Standardeinstellung von GCC ist leider die Aktivierung-ftrapping-math
, obwohl die Implementierung von GCC fehlerhaft / fehlerhaft ist .) Die Int-> Float-Konvertierung kann eine ungenaue FP-Ausnahme verursachen (für Zahlen, die zu groß sind, um genau dargestellt zu werden). Mit möglicherweise nicht maskierten Ausnahmen ist es daher sinnvoll, dies nicht zu tun Optimieren Sie den Schleifenkörper. (Da die Konvertierung16777217
in Float einen beobachtbaren Nebeneffekt haben kann, wenn die ungenaue Ausnahme nicht maskiert ist.)Aber mit
-O3 -fwrapv -fno-trapping-math
, es ist 100% verpasste Optimierung, dies nicht zu einer leeren Endlosschleife zu kompilieren. Ohne#pragma STDC FENV_ACCESS ON
ist der Status der Sticky-Flags, die maskierte FP-Ausnahmen aufzeichnen, kein beobachtbarer Nebeneffekt des Codes. Neinint
->float
Konvertierung kann zu NaN führen,x != x
kann also nicht wahr sein.Diese Compiler sind alle für C ++ - Implementierungen optimiert, die IEEE 754 Single-Precision (Binary32)
float
und 32-Bit verwendenint
.Die Bugfixed-
(int)(float)i != i
Schleife hätte UB in C ++ - Implementierungen mit schmalem 16-Bitint
und / oder breiterfloat
, da Sie vor dem Erreichen der ersten Ganzzahl, die nicht genau als dargestellt werden konnte, den Überlauf UB mit vorzeichenbehafteten Ganzzahlen erreichen würdenfloat
.UB unter einer anderen Reihe von implementierungsdefinierten Auswahlmöglichkeiten hat jedoch keine negativen Konsequenzen beim Kompilieren für eine Implementierung wie gcc oder clang mit dem x86-64 System V ABI.
Übrigens können Sie das Ergebnis dieser Schleife statisch aus
FLT_RADIX
und berechnenFLT_MANT_DIG
, definiert in<climits>
. Zumindest können Sie dies theoretischfloat
tun , wenn es tatsächlich zum Modell eines IEEE-Floats passt und nicht zu einer anderen Art der Darstellung reeller Zahlen wie Posit / Unum.Ich bin mir nicht sicher, wie sehr sich der ISO C ++ - Standard auf das
float
Verhalten auswirkt und ob ein Format, das nicht auf Exponentenfeldern mit fester Breite und Signifikantenfeldern basiert, standardkonform wäre.In Kommentaren:
Wollen Sie behaupten, Sie hätten diese Schleife zum Drucken / Zurückgeben
16777216
?Update: Da dieser Kommentar gelöscht wurde, denke ich nicht. Wahrscheinlich zitiert das OP nur die
float
vor der ersten Ganzzahl, die nicht genau als 32-Bit dargestellt werden kannfloat
. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, dh was sie mit diesem fehlerhaften Code überprüfen wollten.Die Bugfixed-Version würde natürlich drucken
16777217
, die erste Ganzzahl, die nicht genau darstellbar ist, und nicht der Wert davor.(Alle höheren Float-Werte sind exakte Ganzzahlen, aber sie sind Vielfache von 2, dann 4, dann 8 usw. für Exponentenwerte, die höher als die Signifikantenbreite sind. Viele höhere Ganzzahlwerte können dargestellt werden, aber 1 Einheit an letzter Stelle (des Signifikanten) ist größer als 1, es handelt sich also nicht um zusammenhängende ganze Zahlen. Die größte Endlichkeit
float
liegt knapp unter 2 ^ 128, was für gerade zu groß istint64_t
.)Wenn ein Compiler die ursprüngliche Schleife verlassen und diese drucken würde, wäre dies ein Compiler-Fehler.
quelle
frapw
, aber ich bin sicher, dass GCC 10-ffinite-loops
für solche Situationen entwickelt wurde.Beachten Sie, dass der integrierte Operator
!=
verlangt, dass seine Operanden vom gleichen Typ sind, und dies bei Bedarf mithilfe von Promotions und Conversions erreicht. Mit anderen Worten, Ihr Zustand entspricht:Das sollte niemals fehlschlagen, und so läuft der Code irgendwann über
i
, was Ihrem Programm undefiniertes Verhalten verleiht. Jedes Verhalten ist daher möglich.Um zu überprüfen, was Sie überprüfen möchten, sollten Sie das Ergebnis auf Folgendes zurücksetzen
int
:quelle
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
ist offensichtlich UB dort(int)(float)i != i
ist UB? Wie schließen Sie das? Ja, es hängt von den durch die Implementierung definierten Eigenschaften ab (dafloat
IEEE754 binary32 nicht erforderlich ist), aber von jeder Implementierung ist sie gut definiert, es sei denn, siefloat
kann alle positivenint
Werte genau darstellen , sodass wir einen UB-Überlauf mit vorzeichenbehafteten Ganzzahlen erhalten. ( en.cppreference.com/w/cpp/types/climits definiertFLT_RADIX
undFLT_MANT_DIG
bestimmt dies). Im Allgemeinen drucken implementierungsdefinierte Dinge, wiestd::cout << sizeof(int)
ist nicht UB ...reinterpret_cast<int>(float)
ist nicht genau UB, es ist nur ein Syntaxfehler / schlecht geformt. Es wäre schön, wenn diese Syntax das Typ-Punning von Floatint
als Alternative zumemcpy
(was gut definiert ist) zulässt , aberreinterpret_cast<>
nur bei Zeigertypen funktioniert, denke ich.x != x
ist wahr. Live auf Coliru sehen . Auch in C.