web-dev-qa-db-fra.com

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

J'ai essayé de vérifier où float perd la capacité de représenter exactement les 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 boucle infinie simple. Godbolt .

Est-ce permis? Si oui, s'agit-il d'un problème de QoI?

88
geza

Comme l'a souligné @Angew , le != L'opérateur a besoin du même type des deux côtés. (float)i != i entraîne également la promotion du RHS pour qu'il flotte, nous avons donc (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 le voir convertir int-> float avec cvtsi2ss et fait ucomiss xmm0,xmm0 comparer (float)i avec lui-même. (Ce fut votre premier indice que votre source C++ ne signifie pas ce que vous pensiez que cela aimait la réponse de @ Angew.)

x != x n'est vrai que lorsqu'il est "non ordonné" car x était NaN. (INFINITY se compare à lui-même dans les calculs IEEE, mais NaN ne le fait pas. NAN == NAN c'est faux, NAN != NAN est vrai).

gcc7.4 et les versions antérieures optimisent correctement votre code en jnp comme branche de boucle ( https://godbolt.org/z/fyOhW1 ): continuez à boucler tant que les opérandes de x != x n'était pas NaN. (gcc8 et versions ultérieures vérifient également je pour une rupture de la boucle, échouant à l'optimisation en raison du fait qu'il sera toujours vrai pour toute entrée non NaN). x86 FP compare set PF on unordered.


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 les mêmes, et prouver que i -> float n'est jamais NaN pour la plage possible de int.

(Bien que cette boucle atteigne l'UB à débordement signé, elle est autorisée à émettre littéralement tout asm qu'elle souhaite, y compris un ud2 instruction illégale, ou une boucle infinie vide quel que soit le corps de la boucle.) Mais en ignorant l'UB à débordement signé, cette optimisation est toujours 100% légale.


GCC ne parvient pas à optimiser le corps de la boucle même avec -fwrapv pour que le débordement d'entier signé soit bien défini (en guise de complément à 2). https://godbolt.org/z/t9A8t_

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

Mais avec -O3 -fwrapv -fno-trapping-math, c'est 100% d'optimisation manquée de ne pas le compiler dans une boucle infinie vide. Sans pour autant #pragma STDC FENV_ACCESS ON, l'état des drapeaux collants qui enregistrent les exceptions masquées FP n'est pas un effet secondaire observable du code. Pas de conversion int -> float peut entraîner NaN, donc x != x ne peut pas être vrai.


Ces compilateurs optimisent tous les implémentations C++ qui utilisent la précision simple IEEE 754 (binaire32) float et 32 ​​bits int.

Le bugfixed (int)(float)i != i boucle aurait UB sur les implémentations C++ avec étroite 16 bits int et/ou plus large float, parce que vous frapperiez UB de dépassement d'entier signé 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 le x86-64 System V ABI.


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

Je ne sais pas dans quelle mesure la norme ISO C++ cloue sur le comportement de float et si un format qui n'était pas basé sur des champs d'exposant et de signification à 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. L'OP cite probablement le float avant le premier entier qui ne peut pas être représenté exactement comme un float 32 bits. 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 buggy.

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

(Toutes les valeurs flottantes les plus élevées sont des entiers exacts, mais ce sont des multiples de 2, puis 4, puis 8, etc. pour des valeurs d'exposant supérieures à la largeur de la signification. De nombreuses valeurs entières plus élevées peuvent être représentées, mais 1 unité en dernier lieu (de la signification) est supérieur à 1, il ne s'agit donc pas d'entiers contigus. Le plus grand float fini est juste en dessous de 2 ^ 128, ce qui est trop grand pour même int64_t.)

Si un compilateur quittait la boucle d'origine et l'imprimait, ce serait un bogue du compilateur.

47
Peter Cordes

Notez que l'opérateur intégré != requiert que ses opérandes soient du même type et réalisera cela 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 reconvertir le résultat en int:

if ((int)(float)i != i)