1 votes

L'ordre des arguments de std::min change la sortie du compilateur pour les points flottants.

J'ai bricolé dans l'Explorateur de compilateurs, et j'ai découvert que l'ordre des arguments passés à std::min modifie l'assemblage émis.

Voici l'exemple sur Godbolt Compiler Explorer,l:%275%27,n:%270%27,o:%27C%2B%2B+source+%235%27,t:%270%27)),k:50,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27),(g:!((h:compiler,i:(compiler:clang900,filters:(b:%270%27,binary:%271%27,commentOnly:%270%27,demangle:%270%27,directives:%270%27,execute:%271%27,intel:%270%27,libraryCode:%271%27,trim:%271%27),fontScale:14,j:1,lang:c%2B%2B,libs:!(),options:%27-O3%27,selection:(endColumn:1,endLineNumber:1,positionColumn:1,positionLineNumber:1,selectionStartColumn:1,selectionStartLineNumber:1,startColumn:1,startLineNumber:1),source:5),l:%275%27,n:%270%27,o:%27x86-64+clang+9.0.0+(Editor+%235,+Compiler+%231)+C%2B%2B%27,t:%270%27)),header:(),k:50,l:%274%27,n:%270%27,o:%27%27,s:0,t:%270%27)),l:%272%27,n:%270%27,o:%27%27,t:%270%27)),version:4)

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);
}

Ceci est compilé (avec -O3 sur clang 9.0.0, par exemple), en :

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

Cela persiste si je change le std::min en un opérateur ternaire de la vieille école. Il persiste également avec tous les compilateurs modernes que j'ai essayés (clang, gcc, icc).

L'instruction sous-jacente est minsd . En lisant la documentation, le premier argument de minsd est également la destination de la réponse. Apparemment, xmm0 est l'endroit où ma fonction est censée mettre sa valeur de retour, donc si xmm0 est utilisé comme premier argument, il n'y a pas de réponse. movapd nécessaire. Mais si xmm0 est le deuxième argument, alors il faut movapd xmm0, xmm1 pour obtenir la valeur dans xmm0. (note de l'éditeur : oui, Système V x86-64 passe les args FP dans xmm0, xmm1, etc., et retourne dans xmm0.)

Ma question : pourquoi le compilateur ne change-t-il pas l'ordre des arguments lui-même, de sorte que cette movapd n'est pas nécessaire ? Il doit sûrement savoir que l'ordre des arguments de minsd ne change pas la réponse ? Y a-t-il un effet secondaire que je n'apprécie pas ?

78voto

Peter Cordes Points 1375

minsd a,b n'est pas commutatif pour certaines valeurs spéciales de FP, et ne l'est pas non plus std::min à moins que vous n'utilisiez -ffast-math .

minsd a,b exactement met en œuvre (a<b) ? a : b y compris tout ce que cela implique à propos de signed-zero et NaN dans la sémantique stricte IEEE-754. (c'est-à-dire qu'il conserve l'opérande source, b sur un ordre quelconque 1 ou équivalent). Comme le souligne Artyer, -0.0 y +0.0 sont égaux (c'est-à-dire -0. < 0. est faux), mais ils sont distincts.

std::min est défini en fonction d'un (a<b) expression de comparaison ( Référence cpp ), avec (a<b) ? a : b comme une mise en œuvre possible, contrairement à std::fmin qui garantit, entre autres, la propagation de NaN à partir de l'un ou l'autre des opérandes. ( fmin provient à l'origine de la bibliothèque mathématique C, et non d'un modèle C++).

Voir Quelle est l'instruction qui donne FP min et max sans branchement sur x86 ? pour beaucoup plus de détails sur minss/minsd / maxss/maxsd (et les intrinsèques correspondants, qui suivent les mêmes règles non-commutatives sauf dans certaines versions de GCC).

Footnote 1 : Rappelez-vous que NaN<b est faux pour toute b et pour n'importe quel prédicat de comparaison, par ex. NaN == b est faux, tout comme NaN > b . Même NaN == NaN est fausse. Quand un ou plusieurs éléments d'une paire sont NaN, ils sont "non ordonnés" l'un par rapport à l'autre.


Avec -ffast-math (pour dire au compilateur de supposer qu'il n'y a pas de NaN, et d'autres hypothèses et approximations), les compilateurs sera optimiser l'une ou l'autre des fonctions en un seul minsd . https://godbolt.org/z/a7oK91

Pour GCC, voir https://gcc.gnu.org/wiki/FloatingPointMath
clang supporte des options similaires, notamment -ffast-math comme un fourre-tout.

Certaines de ces options devraient être activées par presque tout le monde, à l'exception des bases de code héritées bizarres, par ex. -fno-math-errno . (Voir ce Q&A pour en savoir plus sur les optimisations mathématiques recommandées ). Et gcc -fno-trapping-math est une bonne idée car elle ne fonctionne pas complètement de toute façon, bien qu'elle soit activée par défaut (certaines optimisations peuvent encore modifier le nombre d'exceptions FP qui seraient levées si les exceptions étaient démasquées, y compris parfois même de 1 à 0 ou de 0 à non-zéro, IIRC). gcc -ftrapping-math bloque également certaines optimisations qui sont 100% sûres, même en ce qui concerne la sémantique des exceptions, donc c'est plutôt mauvais. Dans le code qui n'utilise pas fenv.h vous ne verrez jamais la différence.

Mais traiter std::min comme commutatif ne peut être accompli qu'avec des options qui supposent l'absence de NaN, et des choses comme ça, donc on ne peut pas dire que ce soit "sûr". pour le code qui se préoccupe de ce qui se passe exactement avec les NaNs, par ex. -ffinite-math-only ne suppose aucun NaN (et aucune infinité)

clang -funsafe-math-optimizations -ffinite-math-only fera l'optimisation que vous recherchez. (unsafe-math-optimizations implique un tas d'options plus spécifiques, y compris ne pas se soucier de la sémantique du zéro signé).

14voto

Artyer Points 3473

Pensez-y : std::signbit(std::min(+0.0, -0.0)) == false && std::signbit(std::min(-0.0, +0.0)) == true .

La seule autre différence est que si les deux arguments sont des NaN (éventuellement différents), le deuxième argument doit être retourné.


Vous pouvez permettre à gcc de réordonner les arguments en utilisant l'option -funsafe-math-optimizations -fno-math-errno optimsations (toutes deux activées par -ffast-math ). unsafe-math-optimizations permet au compilateur de ne pas se soucier du zéro signé, et finite-math-only de ne pas se soucier des NaNs

5voto

Quuxplusone Points 4320

Pour développer les réponses existantes qui disent std::min n'est pas commutatif : voici un exemple concret qui permet de distinguer de manière fiable std_min_xy de 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() évalue à 1 / 0.0 > 0.0 c'est-à-dire INFTY > 0.0 ou true .
distinguish2() évalue à 1 / -0.0 > 0.0 c'est-à-dire -INFTY > 0.0 ou false .
(Tout cela selon les règles de l'IEEE, bien sûr. Je ne pense pas que la norme C++ mandats que les compilateurs préservent ce comportement particulier. Honnêtement, j'ai été surpris que l'expression -0.0 a été évalué à un zéro négatif en premier lieu !

-ffinite-math-only élimine cette façon de faire la différence et -ffinite-math-only -funsafe-math-optimizations élimine complètement la différence dans le codegen .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X