Pourquoi l’optimisation tue-t-elle cette fonction?

Nous avons récemment eu une conférence à l’université sur la programmation de promotions dans plusieurs langues.

Le conférencier a écrit la fonction suivante:

inline u64 Swap_64(u64 x) { u64 tmp; (*(u32*)&tmp) = Swap_32(*(((u32*)&x)+1)); (*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x); return tmp; } 

Bien que je comprenne tout à fait que ce soit aussi un très mauvais style en termes de lisibilité, son point principal était que cette partie du code fonctionnait bien dans le code de production jusqu’à ce qu’elle permette un niveau d’optimisation élevé. Ensuite, le code ne ferait rien.

Il a dit que toutes les assignations à la variable tmp seraient optimisées par le compilateur. Mais pourquoi cela arriverait-il?

Je comprends qu’il y a des circonstances où les variables doivent être déclarées volatiles pour que le compilateur ne les touche pas même s’il pense qu’elles ne sont jamais lues ou écrites mais je ne saurais pas pourquoi cela arriverait ici.

Ce code viole les règles ssortingctes d’alias, ce qui rend illégal l’access à un object via un pointeur d’un type différent, même si l’access via un * char ** est autorisé. Le compilateur est autorisé à supposer que les pointeurs de différents types ne pointent pas vers la même mémoire et s’optimisent en conséquence. Cela signifie également que le code appelle un comportement indéfini et pourrait vraiment faire n’importe quoi.

Comprendre l’aliasing ssortingct est l’une des meilleures références pour ce sujet. Nous pouvons voir que le premier exemple est similaire au code de l’OP:

 uint32_t swap_words( uint32_t arg ) { uint16_t* const sp = (uint16_t*)&arg; uint16_t hi = sp[0]; uint16_t lo = sp[1]; sp[1] = hi; sp[0] = lo; return (arg); } 

L’article explique que ce code viole les règles ssortingctes d’alias car sp est un alias de arg mais qu’ils ont des types différents et que même s’il sera compilé, il est probable que l’ arg sera inchangé après le retour de swap_words . Bien qu’avec des tests simples, je ne parviens pas à reproduire ce résultat avec le code ci-dessus, ni avec le code OP, mais cela ne veut rien dire car il s’agit d’ un comportement indéfini et donc imprévisible.

L’article poursuit en parlant de nombreux cas différents et présente plusieurs solutions de travail, y compris le type-punning à travers une union, qui est bien définie en C99 1 et peut être indéfinie en C ++ mais en pratique supscope par la plupart des compilateurs est la référence de gcc en matière de punition de type . Le sujet précédent But des Unions en C et C ++ va dans les détails évidents. Bien qu’il y ait beaucoup de discussions sur ce sujet, cela semble faire le meilleur travail.

Le code de cette solution est le suivant:

 typedef union { uint32_t u32; uint16_t u16[2]; } U32; uint32_t swap_words( uint32_t arg ) { U32 in; uint16_t lo; uint16_t hi; in.u32 = arg; hi = in.u16[0]; lo = in.u16[1]; in.u16[0] = lo; in.u16[1] = hi; return (in.u32); } 

Pour référence, la section pertinente du projet de norme C99 sur le repliement ssortingct est 6.5 Expressions paragraphe 7 qui dit:

Un object doit avoir sa valeur stockée accessible uniquement par une expression lvalue qui possède l’un des types suivants: 76)

– un type compatible avec le type effectif de l’object,

– une version qualifiée d’un type compatible avec le type effectif de l’object,

– un type qui est le type signé ou non signé correspondant au type effectif de l’object,

– un type qui est le type signé ou non signé correspondant à une version qualifiée du type effectif de l’object,

– un type d’agrégat ou d’union comprenant l’un des types susmentionnés parmi ses membres (y compris, de manière récursive, un membre d’un sous-agrégat ou d’une union contenue), ou

– un type de caractère.

et la note de bas de page 76 dit:

Le but de cette liste est de spécifier les circonstances dans lesquelles un object peut ou non être aliasé.

et la section pertinente du projet de norme C ++ est 3.10 Lvalues ​​et rvalues paragraphe 10

L’article « Type-punning» et «ssortingct-aliasing» donne une introduction plus douce mais moins complète au sujet et C99 revisité donne une parsing approfondie de C99 et de l’aliasing et n’est pas une lecture légère. Cette réponse à Accès à un membre inactif du syndicat – indéfini? passe en revue les détails boueux de la punition de type via une union en C ++ et ne lit pas non plus.


Notes de bas de page:

  1. Citant le commentaire de Pascal Cuoq: […] C99 qui avait été initialement mal formulé, semblant rendre indéfiniment punissables les syndicats. En réalité, les punitions de type syndical sont légales en C89, légales en C11, et il était légal en C99 depuis le début, bien qu’il ait fallu attendre 2004 pour que le comité répare la formulation incorrecte et la publication ultérieure de TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

En C ++, les arguments de pointeur ne sont pas supposés être alias (sauf char* ) s’ils indiquent des types fondamentalement différents ( règles de “aliasing ssortingct” ). Cela permet certaines optimisations.

Ici, u64 tmp n’est jamais modifié en tant que u64 .
Un contenu de u32* est modifié mais peut ne pas être lié à ‘ u64 tmp ‘ et peut donc être considéré comme u64 tmp pour u64 tmp .

g ++ (Ubuntu / Linaro 4.8.1-10ubuntu9) 4.8.1:

 > g++ -Wall -std=c++11 -O0 -o sample sample.cpp > g++ -Wall -std=c++11 -O3 -o sample sample.cpp sample.cpp: In function 'uint64_t Swap_64(uint64_t)': sample.cpp:10:19: warning: dereferencing type-punned pointer will break ssortingct-aliasing rules [-Wssortingct-aliasing] (*(uint32_t*)&tmp) = Swap_32(*(((uint32_t*)&x)+1)); ^ sample.cpp:11:54: warning: dereferencing type-punned pointer will break ssortingct-aliasing rules [-Wssortingct-aliasing] (*(((uint32_t*)&tmp)+1)) = Swap_32(*(uint32_t*) &x); ^ 

Clang 3.4 ne prévient pas dans les niveaux d’optimisation, ce qui est curieux …