Cette optimisation GCC est-elle incorrecte?

J’ai un foncteur qui prend une valeur, le fait doubler, prend le journal et retourne la valeur au type d’origine. Pour les besoins de cette question, le type d’origine et de sortie est float. Voici le code C ++ d’origine:

return static_cast( std::log( static_cast( A ) ) ) 

Lorsque je comstack en mode débogage, tout se passe comme prévu et GCC appelle la fonction de log sous-jacente:

  51:/myfile.h **** return static_cast( std::log( static_cast( A ) ) ); 219133 .loc 112 51 0 219134 0010 488B45F0 movq -16(%rbp), %rax # A, tmp64 219135 0014 F30F1000 movss (%rax), %xmm0 # *A_1(D), D.237346 219136 0018 0F14C0 unpcklps %xmm0, %xmm0 # D.237346, D.237346 219137 001b 0F5AC0 cvtps2pd %xmm0, %xmm0 # D.237346, D.237347 219138 001e E8000000 call log # 219138 00 219139 0023 660F14C0 unpcklpd %xmm0, %xmm0 # D.237347 219140 0027 660F5AC0 cvtpd2ps %xmm0, %xmm0 # D.237347, D.237346 219141 002b F30F1145 movss %xmm0, -20(%rbp) # D.237346, %sfp 219141 EC 219142 0030 8B45EC movl -20(%rbp), %eax # %sfp,  

Cependant, lorsque j’active les optimisations (-O2 -ggdb3 -DNDEBUG), il appelle la fonction logf (???):

  51:/myfile.h **** return static_cast( std::log( static_cast( A ) ) ); 145171 .loc 64 51 0 145172 01a0 F30F1004 movss (%rdx,%rax,4), %xmm0 # MEM[(const float &)_84], MEM[(const float &)_84] 145172 82 145173 01a5 E8000000 call logf # 

Cela donne une sortie différente. Est-ce normal? Est-ce que je fais quelque chose de mal? Il me semble que GCC prend une interprétation très libérale de mon code, ce à quoi je ne m’attendrais pas en l’absence de l’option -ffast-math .

Il s’agit d’une optimisation limite permettant de transformer la conversion en une valeur float d’une application du log à double précision en un flottant à une application du log simple précision, mais on peut affirmer que cela est acceptable.

En supposant que logf est correctement arrondi et que le log double précision est également correctement arrondi ou au moins arrondi, les deux calculs diffèrent rarement. Ils peuvent différer (pour certains intrants rares) en raison du double arrondi (dans lequel “double” signifie “deux fois” et ne fait pas référence au type). Le double arrondi est d’autant moins significatif qu’il existe des chiffres supplémentaires dans le significand du type intermédiaire par rapport au significand du type final (et cet argument statistique est légèrement désagréable d’un sharepoint vue mathématique, mais il “fonctionne en pratique” pour des fonctions qui n’ont pas été conçus pour être des contre-exemples). Pour des raisons didactiques, les gens (ou Wikipedia) l’expliquent avec un ou deux chiffres supplémentaires de précision, mais lorsque vous avez 53 – 24 = 29 chiffres binarys supplémentaires, vous pouvez vous attendre à ce qu’ils se produisent aussi rarement qu’une fois sur 29 .

Je suis surpris par l’optimisation et je serais dérangé si j’avais écrit le code moi-même pour une recherche exhaustive des problèmes de double arrondi avec le log , mais en considérant que le standard C ++ ne demandait aucun niveau de précision à partir de std::log , il est possible de le considérer “pas un bug”.


Si, au lieu de log , nous avions parlé d’une des opérations de base (telle que * ), alors la transformation aurait été incorrecte, pour un compilateur qui prétend fournir la sémantique IEEE 754, lorsqu’il introduit des modifications visibles. Pour une opération de base, la précision est spécifiée indirectement par IEEE 754 et la spécification ne laisse aucune place aux variations.

Il se trouve que pour les opérations de base, il ne peut pas y avoir de changement visible lorsque floatOP(flx,fly) remplace (float)doubleOP((double)flx, (double)fly) (cette thèse le montre au chapitre 6) peuvent être des différences visibles lorsque les types sont double et long double . Ce bogue exact a été récemment corrigé dans Clang par Stephen Canon.

Oui, cette optimisation est incorrecte. log et logf ne doivent être arrondis que de manière fidèle.

 logf(4) = 0x1.62e42p+0 log(4) = 0x1.62e42fefa39efp+0 

La modification de la conversion ascendante, du log et de la conversion vers un appel logf peut donner des résultats incorrects.

La réponse de Pascal Cuoq souligne à juste titre que, si logf est correctement arrondi et que log n’est pas une erreur, les résultats ne seront probablement pas différents. Cependant, logf sur ma plate-forme n’est pas correctement arrondi:

 logf(0x1.306p-138) = -0x1.7decc8p+6 (float)log(0x1.306p-138) = -0x1.7decc6p+6 mpfr_log(0x1.306p-138) = -0x1.7decc6ff8a7a4a4450e9p+6 

Heureusement, je ne peux pas reproduire cette “optimisation” avec mon gcc.

J’ai essayé un programme C équivalent:

 #include  #include  #include  static double dostuff(float in) { return log(in); } int main(int argc, char **argv) { if (argc < 2) exit(EXIT_FAILURE); float in = strtof(argv[1], NULL); float out = dostuff(in); printf("%f\n", out); return 0; } 

Et constaté que gcc n'utilise pas logf même en utilisant -Ofast . La seule chose -Ofast permet d' __log_finite() . Cependant, le changement du type de retour de dostuff() sur float dostuff() cette optimisation.

Vous ne devriez avoir aucune inquiétude concernant les différentes sorties. Si vous transformez une valeur flottante en une valeur double, puis revenez à une valeur flottante; vous pouvez obtenir une valeur différente; par conséquent, la valeur différente que vous obtenez en convertissant un flottant en une valeur double, en exécutant l’opération de journalisation et en renvoyant le résultat à un flottant n’est pas meilleure que la valeur obtenue en appelant directement la fonction logf.

Si vous avez des questions sur la précision de l’opération, vous ne devriez pas utiliser le type float et toutes vos valeurs mathématiques devraient être doubles.

En utilisant le type float, vous signalez au compilateur que vous n’avez aucune attention particulière à la précision finale de vos opérations mathématiques au-delà de celle fournie par le type float. Autrement dit, lorsque vous effectuez des opérations mathématiques avec des nombres réels (des nombres autres que des nombres entiers), la précision ne peut soit restr identique ou diminuer; il ne peut jamais augmenter; par conséquent, si vous commencez avec un type flottant, vous ne pouvez pas augmenter votre précision au-delà de ce type.