J'ai essayé de vérifier où float
perd 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?
c++
floating-point
clang
geza
la source
la source
gcc
fait la même optimisation de boucles infinies si vous compilez avec à la-Ofast
place, donc c'est une optimisation qui n'estgcc
pas sûre, mais elle peut le faire.ucomiss xmm0,xmm0
se 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 / renvoyer16777216
? Avec quel compilateur / version / options était-ce? Parce que ce serait un bogue du compilateur. gcc optimise correctement votre code enjnp
tant que branche de boucle ( godbolt.org/z/XJYWeu ): continuez à boucler tant que les opérandes!=
ne sont pas NaN.-ffast-math
option qui est implicitement activée par-Ofast
qui 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 seulejmp
instruction. 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(float) i
différait de la valeur mathématique dei
, le résultat (la valeur renvoyée dans l'return
instruction) serait 16 777 217 et non 16 777 216.Réponses:
Comme @Angew l'a souligné , l'
!=
opérateur a besoin du même type des deux côtés.(float)i != i
se 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
cvtsi2ss
etucomiss xmm0,xmm0
se compare(float)i
avec 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 != x
est seulement vrai quand il est "non ordonné" parce quex
c'était NaN. (seINFINITY
compare égal à lui-même en mathématiques IEEE, mais NaN ne le fait pas.NAN == NAN
est faux,NAN != NAN
est vrai).gcc7.4 et les versions antérieures optimisent correctement votre code en
jnp
tant que branche de boucle ( https://godbolt.org/z/fyOhW1 ): continuez à boucler tant que les opérandesx != x
ne sont pas NaN. (gcc8 et les versions ultérieures vérifient égalementje
une 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)i
comme étant le même, et de prouver que cei -> float
n'est jamais NaN pour la plage possible deint
.(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
ud2
instruction 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
-fwrapv
dé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-math
n'aide pas. (La valeur par défaut de GCC est malheureusement d'activer-ftrapping-math
mê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 conversion16777217
en 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. Nonint
-> lafloat
conversion peut entraîner NaN, doncx != x
cela ne peut pas être vrai.Ces compilateurs sont tous optimisés pour les implémentations C ++ qui utilisent IEEE 754 simple précision (binary32)
float
et 32 bitsint
.La boucle corrigée d'
(int)(float)i != i
un bogue aurait UB sur les implémentations C ++ avec 16 bits étroitint
et / ou plus largefloat
, 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 quefloat
.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_RADIX
etFLT_MANT_DIG
, défini dans<climits>
. Ou du moins vous pouvez en théorie, sifloat
correspond 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
float
comportement 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:
Ê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'
float
avant du premier entier qui ne peut pas être exactement représenté sous forme de 32 bitsfloat
. 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
float
est juste en dessous de 2 ^ 128, ce qui est trop grand pour pairint64_t
.)Si un compilateur sortait de la boucle d'origine et l'affichait, ce serait un bogue du compilateur.
la source
frapw
, mais je suis sûr que GCC 10 a-ffinite-loops
été conçu pour des situations comme celle-ci.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 à: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
:la source
static_cast<int>(static_cast<float>(i))
?reinterpret_cast
est évidente UB là(int)(float)i != i
c'est UB? Comment concluez-vous cela? Oui, cela dépend des propriétés définies par l'implémentation (car ilfloat
n'est pas nécessaire d'être IEEE754 binary32), mais pour toute implémentation donnée, elle est bien définie à moins qu'elle nefloat
puisse représenter exactement toutes lesint
valeurs positives afin que nous obtenions un débordement d'entier signé UB. ( fr.cppreference.com/w/cpp/types/climits définitFLT_RADIX
etFLT_MANT_DIG
détermine cela). En général, l'impression de choses définies par l'implémentation, commestd::cout << sizeof(int)
n'est pas UB ...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 toint
comme alternative àmemcpy
(qui est bien défini), maisreinterpret_cast<>
ne fonctionne que sur les types de pointeurs, je pense.x != x
c'est vrai. Voir en direct sur coliru . En C aussi.