Cette optimisation en virgule flottante est-elle autorisée?

90

J'ai essayé de vérifier où floatperd la capacité de représenter exactement de grands nombres entiers. J'ai donc écrit ce petit extrait:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Ce code semble fonctionner avec tous les compilateurs, sauf clang. Clang génère une simple boucle infinie. Godbolt .

Est-ce permis? Si oui, est-ce un problème de QoI?

geza
la source
@geza Je serais intéressé d'entendre le nombre résultant!
nada
5
gccfait la même optimisation de boucles infinies si vous compilez avec à la -Ofastplace, donc c'est une optimisation qui n'est gccpas sûre, mais elle peut le faire.
12345ieee
3
g ++ génère également une boucle infinie, mais il n'optimise pas le travail de l'intérieur. Vous pouvez le voir ucomiss xmm0,xmm0se comparer (float)ià lui-même. C'était votre premier indice que votre source C ++ ne signifie pas ce que vous pensiez qu'elle faisait. Êtes-vous en train de prétendre que vous avez cette boucle à imprimer / renvoyer 16777216? Avec quel compilateur / version / options était-ce? Parce que ce serait un bogue du compilateur. gcc optimise correctement votre code en jnptant que branche de boucle ( godbolt.org/z/XJYWeu ): continuez à boucler tant que les opérandes != ne sont pas NaN.
Peter Cordes
4
Plus précisément, c'est l' -ffast-mathoption qui est implicitement activée par -Ofastqui permet à GCC d'appliquer des optimisations à virgule flottante non sécurisées et ainsi de générer le même code que Clang. MSVC se comporte exactement de la même manière: sans /fp:fast, il génère un tas de code qui aboutit à une boucle infinie; avec /fp:fast, il émet une seule jmpinstruction. Je suppose que sans activer explicitement les optimisations FP non sécurisées, ces compilateurs se bloquent sur les exigences IEEE 754 concernant les valeurs NaN. Plutôt intéressant que Clang ne le fasse pas, en fait. Son analyseur statique est meilleur. @ 12345ieee
Cody Gray
1
@geza: si le code a fait ce que vous vouliez, en vérifiant si la valeur mathématique de (float) idifférait de la valeur mathématique de i, le résultat (la valeur renvoyée dans l' returninstruction) serait 16 777 217 et non 16 777 216.
Eric Postpischil

Réponses:

49

Comme @Angew l'a souligné , l' !=opérateur a besoin du même type des deux côtés. (float)i != ise traduit également par la promotion de la RHS à flotter, c'est ce que nous avons fait (float)i != (float)i.


g ++ génère également une boucle infinie, mais il n'optimise pas le travail de l'intérieur. Vous pouvez voir qu'il convertit int-> float avec cvtsi2sset ucomiss xmm0,xmm0se compare (float)iavec lui-même. (C'était votre premier indice que votre source C ++ ne signifie pas ce que vous pensiez qu'elle faisait, comme l'explique la réponse de @ Angew.)

x != xest seulement vrai quand il est "non ordonné" parce que xc'était NaN. (se INFINITYcompare égal à lui-même en mathématiques IEEE, mais NaN ne le fait pas. NAN == NANest faux, NAN != NANest vrai).

gcc7.4 et les versions antérieures optimisent correctement votre code en jnptant que branche de boucle ( https://godbolt.org/z/fyOhW1 ): continuez à boucler tant que les opérandes x != x ne sont pas NaN. (gcc8 et les versions ultérieures vérifient également jeune rupture de la boucle, échouant à l'optimisation en se basant sur le fait que ce sera toujours vrai pour toute entrée non-NaN). x86 FP compare l'ensemble PF sur non ordonné.


Et BTW, cela signifie que l'optimisation de clang est également sûre : il suffit de CSE (float)i != (implicit conversion to float)icomme étant le même, et de prouver que ce i -> floatn'est jamais NaN pour la plage possible de int.

(Bien que cette boucle atteigne UB de débordement signé, il est autorisé à émettre littéralement n'importe quel asm qu'il veut, y compris une ud2instruction illégale, ou une boucle infinie vide quel que soit le corps de la boucle.) Mais en ignorant le dépassement signé UB , cette optimisation est toujours légale à 100%.


GCC ne parvient pas à optimiser le corps de la boucle, même si le -fwrapvdébordement d'entier signé est bien défini (en tant que complément à 2). https://godbolt.org/z/t9A8t_

Même l'activation -fno-trapping-mathn'aide pas. (La valeur par défaut de GCC est malheureusement d'activer
-ftrapping-mathmême si l'implémentation de GCC est cassée / boguée .) La conversion int-> float peut provoquer une exception FP inexacte (pour des nombres trop grands pour être représentés exactement), donc avec des exceptions éventuellement démasquées, il est raisonnable de ne pas optimisez le corps de la boucle. (Parce que la conversion 16777217en float pourrait avoir un effet secondaire observable si l'exception inexacte est démasquée.)

Mais avec -O3 -fwrapv -fno-trapping-math, c'est une optimisation ratée à 100% de ne pas compiler cela en une boucle infinie vide. Sans #pragma STDC FENV_ACCESS ON, l'état des indicateurs persistants qui enregistrent les exceptions FP masquées n'est pas un effet secondaire observable du code. Non int-> la floatconversion peut entraîner NaN, donc x != xcela ne peut pas être vrai.


Ces compilateurs sont tous optimisés pour les implémentations C ++ qui utilisent IEEE 754 simple précision (binary32) floatet 32 ​​bits int.

La boucle corrigée d'(int)(float)i != i un bogue aurait UB sur les implémentations C ++ avec 16 bits étroit intet / ou plus large float, car vous auriez atteint un débordement d'entier signé UB avant d'atteindre le premier entier qui n'était pas exactement représentable en tant que float.

Mais UB sous un ensemble différent de choix définis par l'implémentation n'a pas de conséquences négatives lors de la compilation pour une implémentation comme gcc ou clang avec l'ABI x86-64 System V.


BTW, vous pouvez calculer statiquement le résultat de cette boucle à partir de FLT_RADIXet FLT_MANT_DIG, défini dans <climits>. Ou du moins vous pouvez en théorie, si floatcorrespond réellement au modèle d'un flotteur IEEE plutôt qu'à un autre type de représentation en nombre réel comme un Posit / unum.

Je ne sais pas à quel point la norme ISO C ++ résout le floatcomportement et si un format qui n'était pas basé sur des champs d'exposant et de significand à largeur fixe serait conforme aux normes.


Dans les commentaires:

@geza Je serais intéressé d'entendre le nombre résultant!

@nada: c'est 16777216

Êtes-vous en train de prétendre que vous avez cette boucle à imprimer / renvoyer 16777216?

Mise à jour: puisque ce commentaire a été supprimé, je ne pense pas. Il est probable que l'OP ne cite que l' floatavant du premier entier qui ne peut pas être exactement représenté sous forme de 32 bits float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values c'est à dire ce qu'ils espéraient vérifier avec ce code bogué.

La version corrigée du bogue afficherait bien sûr 16777217, le premier entier qui n'est pas exactement représentable, plutôt que la valeur avant cela.

(Toutes les valeurs flottantes supérieures sont des entiers exacts, mais ce sont des multiples de 2, puis 4, puis 8, etc. pour les valeurs d'exposant supérieures à la largeur du significand. De nombreuses valeurs entières plus élevées peuvent être représentées, mais 1 unité à la dernière place (du significande) est supérieur à 1, donc ce ne sont pas des entiers contigus. Le plus grand fini floatest juste en dessous de 2 ^ 128, ce qui est trop grand pour pair int64_t.)

Si un compilateur sortait de la boucle d'origine et l'affichait, ce serait un bogue du compilateur.

Peter Cordes
la source
3
@SombreroChicken: non, j'ai d'abord appris l'électronique (à partir de certains manuels que mon père avait traîné; il était professeur de physique), puis la logique numérique et je suis ensuite entré dans les processeurs / logiciels. : P Tellement j'ai toujours aimé comprendre les choses de A à Z, ou si je commence par un niveau supérieur, j'aime apprendre au moins quelque chose sur le niveau inférieur qui influence comment / pourquoi les choses fonctionnent dans le niveau où je suis penser. (par exemple, comment asm fonctionne et comment l'optimiser est influencé par les contraintes de conception du processeur / de l'architecture du processeur. Ce qui à son tour vient de la physique + des mathématiques.)
Peter Cordes
1
GCC pourrait ne pas être en mesure d'optimiser même avec frapw, mais je suis sûr que GCC 10 a -ffinite-loopsété conçu pour des situations comme celle-ci.
MCCCS
64

Notez que l'opérateur intégré !=nécessite que ses opérandes soient du même type, et y parviendra en utilisant des promotions et des conversions si nécessaire. En d'autres termes, votre condition équivaut à:

(float)i != (float)i

Cela ne devrait jamais échouer, et donc le code finira par déborder i, donnant à votre programme un comportement indéfini. Tout comportement est donc possible.

Pour vérifier correctement ce que vous voulez vérifier, vous devez convertir le résultat en int:

if ((int)(float)i != i)
Angew n'est plus fier de SO
la source
8
@ Džuris C'est UB. Il n'y a pas de résultat définitif. Le compilateur peut se rendre compte qu'il ne peut se terminer que par UB et décider de supprimer complètement la boucle.
Fund Monica's Lawsuit
4
@opa voulez-vous dire static_cast<int>(static_cast<float>(i))? reinterpret_castest évidente UB là
Caleth
6
@NicHartley: Êtes-vous en train de dire que (int)(float)i != ic'est UB? Comment concluez-vous cela? Oui, cela dépend des propriétés définies par l'implémentation (car il floatn'est pas nécessaire d'être IEEE754 binary32), mais pour toute implémentation donnée, elle est bien définie à moins qu'elle ne floatpuisse représenter exactement toutes les intvaleurs positives afin que nous obtenions un débordement d'entier signé UB. ( fr.cppreference.com/w/cpp/types/climits définit FLT_RADIXet FLT_MANT_DIGdétermine cela). En général, l'impression de choses définies par l'implémentation, comme std::cout << sizeof(int)n'est pas UB ...
Peter Cordes
2
@Caleth: reinterpret_cast<int>(float)n'est pas exactement UB, c'est juste une erreur de syntaxe / mal formée. Ce serait bien si cette syntaxe permettait le poinçonnage de type float to intcomme alternative à memcpy(qui est bien défini), mais reinterpret_cast<>ne fonctionne que sur les types de pointeurs, je pense.
Peter Cordes
2
@Peter Juste pour NaN, x != xc'est vrai. Voir en direct sur coliru . En C aussi.
Deduplicator du