Die Argumentreihenfolge in std :: min ändert die Compilerausgabe für Gleitkommawerte

74

Ich habe im Compiler-Explorer herumgespielt und festgestellt, dass die Reihenfolge der an std :: min übergebenen Argumente die ausgegebene Assembly ändert.

Hier ist das Beispiel im Godbolt Compiler Explorer

double std_min_xy(double x, double y) {
    return std::min(x, y);
}

double std_min_yx(double x, double y) {
    return std::min(y, x);
}

Dies wird kompiliert (zum Beispiel mit -O3 in Clang 9.0.0), um:

std_min_xy(double, double):                       # @std_min_xy(double, double)
        minsd   xmm1, xmm0
        movapd  xmm0, xmm1
        ret
std_min_yx(double, double):                       # @std_min_yx(double, double)
        minsd   xmm0, xmm1
        ret

Dies bleibt bestehen, wenn ich std :: min in einen ternären Operator der alten Schule ändere. Es bleibt auch bei allen modernen Compilern bestehen, die ich ausprobiert habe (clang, gcc, icc).

Die zugrunde liegende Anweisung lautet minsd. Beim Lesen der Dokumentation ist das erste Argument von minsdauch das Ziel für die Antwort. Anscheinend ist xmm0 der Ort, an dem meine Funktion ihren Rückgabewert setzen soll. Wenn also xmm0 als erstes Argument verwendet wird, ist dies nicht movapderforderlich. Aber wenn xmm0 das zweite Argument ist, muss es movapd xmm0, xmm1den Wert in xmm0 bekommen. (Anmerkung des Herausgebers: Ja, x86-64 System V übergibt FP-Argumente in xmm0, xmm1 usw. und gibt in xmm0 zurück.)

Meine Frage: Warum ändert der Compiler nicht die Reihenfolge der Argumente selbst, so dass dies movapdnicht notwendig ist? Es muss sicher wissen, dass die Reihenfolge der Argumente zu minsd die Antwort nicht ändert? Gibt es eine Nebenwirkung, die ich nicht schätze?

RaveTheTadpole
quelle
Wahrscheinlich einfach, weil das Speichern eines einzelnen Registertauschs in einem sehr seltenen Fall die Mühe nicht wert ist
Alan Birtles
12
@AlanBirtles Ich würde hoffen, dass die Leute, die Compiler-Optimierungen schreiben, nicht so denken. Es schmerzt mich. Ich versuche, mich nicht darum zu kümmern, weil es in meinem Kontext keine Rolle spielt, aber es tut trotzdem weh.
RaveTheTadpole
2
@AlanBirtles: Rave ist richtig, so denken Compiler-Entwickler nicht. Wenn dies wirklich eine verpasste Optimierung wäre (anstatt von der strengen FP-Semantik gefordert), würden Entwickler von gcc und clang es wahrscheinlich begrüßen, wenn ein Fehler bei der fehlenden Optimierung gemeldet würde. (Obwohl dies wahrscheinlich bereits ein bekannter Fehler wäre; Einschränkungen durch das Aufrufen von Konventionsanforderungen führen manchmal zu Verschwendung movoder movapsAnweisungen, wenn gcc die Zuweisung registriert, was Sie nach dem Inlining nicht in der Mitte einer größeren Funktion sehen würden.)
Peter Cordes
2
@bolov Ich meinte, dass der Compiler die Reihenfolge der minsdOperanden hätte ändern können, um die zu speichern movapd. (Aber während ich lerne, kann es das nicht.)
RaveTheTadpole
Warum wird dieser Frage das Android-Tag hinzugefügt?
Peter Haddad

Antworten:

78

minsd a,bist für einige spezielle FP-Werte nicht kommutativ und auch nichtstd::min , es sei denn, Sie verwenden -ffast-math.

minsd a,b implementiert genau(a<b) ? a : b alles, was über vorzeichenbehaftete Null und NaN in der strengen IEEE-754-Semantik impliziert. (dh es hält den Quelloperanden bauf ungeordnet 1 oder gleich). Wie Artyer betont -0.0und +0.0gleich vergleicht (dh -0. < 0.falsch ist), aber sie sind verschieden.

std::minwird als (a<b)Vergleichsausdruck ( cppreference ) definiert, mit (a<b) ? a : beiner möglichen Implementierung, im Gegensatz dazu, std::fmindie unter anderem die NaN-Ausbreitung von beiden Operanden garantiert. ( fminstammte ursprünglich aus der C-Mathematikbibliothek, nicht aus einer C ++ - Vorlage.)

Siehe Was ist die Anweisung, die verzweigungslose FP min und max auf x86 gibt? Weitere Informationen zu minss / minsd / maxss / maxsd (und den entsprechenden Intrinsics, die mit Ausnahme einiger GCC-Versionen denselben nicht kommutativen Regeln folgen).

Fußnote 1: Denken Sie daran, dass dies NaN<bfür jedes bund für jedes Vergleichsprädikat falsch ist . zB NaN == bist falsch, und so ist NaN > b. Auch NaN == NaNist falsch. Wenn eines oder mehrere eines Paares NaN sind, sind sie "ungeordnet". gegenseitig.


Mit -ffast-math(dem Compiler mitzuteilen , keine NaNs und andere Annahmen und Annäherungen zu übernehmen), Compiler wird entweder Funktion auf einen einzigen optimieren minsd. https://godbolt.org/z/a7oK91

Informationen zu GCC finden Sie unter https://gcc.gnu.org/wiki/FloatingPointMath.
Clang unterstützt ähnliche Optionen, auch -ffast-mathals Sammelbegriff.

Einige dieser Optionen sollten von fast jedem aktiviert werden, mit Ausnahme von seltsamen alten Codebasen, z -fno-math-errno. (Weitere Informationen zu empfohlenen mathematischen Optimierungen finden Sie in diesen Fragen und Antworten. ) Und gcc -fno-trapping-mathist eine gute Idee, da es ohnehin nicht vollständig funktioniert, obwohl es standardmäßig aktiviert ist (einige Optimierungen können immer noch die Anzahl der FP-Ausnahmen ändern, die ausgelöst würden, wenn Ausnahmen entlarvt würden, manchmal sogar von 1 auf 0 oder 0 auf ungleich Null, IIRC). gcc -ftrapping-mathblockiert auch einige Optimierungen, die auch für 100% sicher sind. Ausnahmesemantik, also ist es ziemlich schlecht. In Code, der nicht verwendet wird fenv.h, werden Sie den Unterschied nie erfahren.

Die Behandlung std::minals kommutativ kann jedoch nur mit Optionen erreicht werden, die keine NaNs und ähnliches voraussetzen. Daher kann sie für Code, der sich genau darum kümmert, was mit NaNs passiert , definitiv nicht als "sicher" bezeichnet werden. zB -ffinite-math-onlynimmt keinerlei NaNs (und keiner infinities)

clang -funsafe-math-optimizations -ffinite-math-onlywird die Optimierung durchführen, die Sie suchen. (Unsichere mathematische Optimierungen implizieren eine Reihe spezifischerer Optionen, einschließlich der Nichtbeachtung der vorzeichenbehafteten Nullsemantik).

Peter Cordes
quelle
8
Einige der Details, die -ffast-mathignoriert werden, sind nicht so subtil. Ich war unangenehm überrascht, dass es optimiert wird isnan()für false: godbolt.org/z/zs31Yn
jpa
1
@jpa: Ja, "subtil" war keine großartige Beschreibung, außer für Code, bei dem NaN unter normalen Bedingungen nicht vorkommt und Sie nicht versuchen, damit umzugehen. Um genauer zu sein.
Peter Cordes
2
Beachten Sie, dass die Notwendigkeit movapdmit -mavxOption behoben wird (vorausgesetzt, die Ziel-CPU unterstützt AVX), da AVX eine zerstörungsfreie Quellcodierung (3-Operanden) der Anweisungen hinzufügt.
Ruslan
1
@ Ruslan: Stimmt. Sie könnten jedoch immer noch Fälle erfinden, in denen es eine zusätzliche Anweisung kostet, z. B. in denen ein Operand speicherartig ist x = std::min(array[i], x). Sogar AVX würde ein separates Laden in einer Schleife anstelle eines Speicherquellenoperanden benötigen, da nur die 2. Quelle Speicher sein kann. (Und es konnte nicht automatisch mit einer horizontalen Minute am Ende vektorisiert werden: std::minist auch für FP nicht assoziativ).
Peter Cordes
14

Bedenken Sie : std::signbit(std::min(+0.0, -0.0)) == false && std::signbit(std::min(-0.0, +0.0)) == true.

Der einzige andere Unterschied besteht darin, dass das zweite Argument zurückgegeben werden sollte, wenn beide Argumente (möglicherweise unterschiedliche) NaNs sind.


Sie können gcc erlauben, die Argumente mithilfe der -funsafe-math-optimizations -fno-math-errnoOptimierungen neu anzuordnen (beide aktiviert durch -ffast-math). unsafe-math-optimizationsErmöglicht dem Compiler, sich nicht um vorzeichenbehaftete finite-math-onlyNullen und nicht um NaNs zu kümmern

Artyer
quelle
1
-fno-math-errnosollte hier irrelevant sein; std::minist eine C ++ STL-Vorlagenfunktion, keine C-Mathematikbibliotheksfunktion, die in der Vergangenheit eine Fehlereinstellung für die NaN-Semantik wie fminoder hatte sqrt. -fno-math-errnoist jedoch immer eine gute Idee, außer in historischen Codebasen, die prüfen errnoanstatt zu verwenden fenv.h.
Peter Cordes
1
Sollte aber anscheinend nicht sein, mit clang9.0. godbolt.org/z/4bdvMa zeigt, dass dies -funsafe-math-optimizationsallein nicht funktioniert, aber durch Hinzufügen -fno-math-errnowird es als kommutativ optimiert. Dies kann ein clang9-Fehler sein, der möglicherweise auch eine No-NaN-Annahme impliziert, die unsichere Mathematik nicht beinhaltet? Mit clang 10.0 und mit GCC behält unsafe-math + no-math-errno immer noch den Unterschied in der Reihenfolge der Operanden bei: Ein anderer Teil von fast-math macht den Unterschied.
Peter Cordes
5

Zur Erweiterung der bestehenden Antworten , die sagen std::minnicht kommutativ ist: Hier ist ein konkretes Beispiel dafür , dass zuverlässig unterscheidet std_min_xyvon std_min_yx. Godbolt:

bool distinguish1() {
    return 1 / std_min_xy(0.0, -0.0) > 0.0;
}
bool distinguish2() {
    return 1 / std_min_yx(0.0, -0.0) > 0.0;
}

distinguish1()bewertet zu 1 / 0.0 > 0.0, dh INFTY > 0.0oder true.
distinguish2()bewertet zu 1 / -0.0 > 0.0, dh -INFTY > 0.0oder false.
(All dies unter IEEE Regeln, natürlich. Ich glaube nicht , die C ++ Standard Mandate , dass Compiler dieses besondere Verhalten bewahren. Ehrlich gesagt war ich überrascht , dass der Ausdruck -0.0ausgewertet tatsächlich zu einem negativen Null an erster Stelle!

-ffinite-math-onlybeseitigt diese Art, den Unterschied zu erkennen , und -ffinite-math-only -funsafe-math-optimizationsbeseitigt den Unterschied im Codegen vollständig .

Quuxpluson
quelle