Ceci est un bug Clang
... lors de l'insertion d'une fonction contenant une boucle infinie. Le comportement est différent lorsqu'il while(1);
apparaît directement en main, ce qui me sent très bogué.
Voir la réponse de @ Arnavion pour un résumé et des liens. Le reste de cette réponse a été écrit avant que je ne confirme que c'était un bug, encore moins un bug connu.
Pour répondre à la question du titre: Comment créer une boucle vide infinie qui ne sera pas optimisée? ? -
créer die()
une macro, pas une fonction , pour contourner ce bogue dans Clang 3.9 et versions ultérieures. (Les versions antérieures de Clang conservent la boucle ou émettent uncall
vers une version non en ligne de la fonction avec la boucle infinie.) Cela semble être sûr même si la print;while(1);print;
fonction s'aligne dans son appelant ( Godbolt ). -std=gnu11
vs -std=gnu99
ne change rien.
Si vous ne vous souciez que de GNU C, P__J____asm__("");
à l'intérieur de la boucle fonctionne également, et ne devrait pas nuire à l'optimisation du code environnant pour les compilateurs qui le comprennent. Les instructions asm GNU C Basic sont implicitementvolatile
, donc cela compte comme un effet secondaire visible qui doit "s'exécuter" autant de fois que dans la machine abstraite C. (Et oui, Clang implémente le dialecte GNU de C, comme documenté par le manuel GCC.)
Certaines personnes ont fait valoir qu'il pourrait être légal d'optimiser une boucle infinie vide. Je ne suis pas d'accord 1 , mais même si nous acceptons cela, il ne peut pas également être légal pour Clang de supposer que les instructions après que la boucle soit inaccessible, et de laisser l'exécution tomber de la fin de la fonction dans la fonction suivante, ou dans les ordures qui décode comme des instructions aléatoires.
(Ce serait conforme aux normes pour Clang ++ (mais toujours pas très utile); les boucles infinies sans aucun effet secondaire sont UB en C ++, mais pas C.
Is while (1); comportement indéfini en C? UB permet au compilateur d'émettre pratiquement n'importe quoi pour le code sur un chemin d'exécution qui rencontrera certainement l'UB. Une asm
instruction dans la boucle éviterait cet UB pour C ++. Mais dans la pratique, la compilation de Clang en C ++ ne supprime pas les boucles vides infinies d'expression constante, sauf en ligne, comme lorsque compilation en C.)
L'intégration manuelle while(1);
modifie la façon dont Clang le compile: boucle infinie présente dans asm. C'est ce que nous attendons d'un PDV de règles-avocat.
#include <stdio.h>
int main() {
printf("begin\n");
while(1);
//infloop_nonconst(1);
//infloop();
printf("unreachable\n");
}
Sur l'explorateur du compilateur Godbolt , Clang 9.0 -O3 se compile en C ( -xc
) pour x86-64:
main: # @main
push rax # re-align the stack by 16
mov edi, offset .Lstr # non-PIE executable can use 32-bit absolute addresses
call puts
.LBB3_1: # =>This Inner Loop Header: Depth=1
jmp .LBB3_1 # infinite loop
.section .rodata
...
.Lstr:
.asciz "begin"
Le même compilateur avec les mêmes options compile un main
qui appelle infloop() { while(1); }
d'abord le même puts
, mais arrête ensuite d'émettre des instructions pour main
après ce point. Donc, comme je l'ai dit, l'exécution tombe juste de la fin de la fonction, quelle que soit la fonction suivante (mais avec la pile mal alignée pour l'entrée de la fonction, ce n'est donc même pas un appel valide).
Les options valables seraient de
- émettre une
label: jmp label
boucle infinie
- ou (si nous acceptons que la boucle infinie puisse être supprimée) émettre un autre appel pour imprimer la 2ème chaîne, puis à
return 0
partir de main
.
Crasher ou continuer sans imprimer "inaccessible" n'est clairement pas acceptable pour une implémentation C11, sauf s'il y a UB que je n'ai pas remarqué.
Note de bas de page 1:
Pour mémoire, je suis d'accord avec la réponse de @ Lundin qui cite la norme pour prouver que C11 ne permet pas l'hypothèse de terminaison pour les boucles infinies à expression constante, même lorsqu'elles sont vides (pas d'E / S, volatiles, synchronisation, ou autre effets secondaires visibles).
Il s'agit de l'ensemble de conditions permettant de compiler une boucle en une boucle asm vide pour un processeur normal. (Même si le corps n'était pas vide dans la source, les affectations aux variables ne peuvent pas être visibles par les autres threads ou les gestionnaires de signaux sans UB de course de données pendant que la boucle est en cours d'exécution. Une implémentation conforme pourrait donc supprimer ces corps de boucle si elle le voulait Ensuite, cela laisse la question de savoir si la boucle elle-même peut être supprimée. ISO C11 dit explicitement non.)
Étant donné que C11 distingue ce cas comme un cas où l'implémentation ne peut pas supposer que la boucle se termine (et que ce n'est pas UB), il semble clair qu'ils ont l'intention que la boucle soit présente au moment de l'exécution. Une implémentation qui cible les processeurs avec un modèle d'exécution qui ne peut pas faire une quantité infinie de travail en temps fini n'a aucune justification pour supprimer une boucle infinie constante vide. Ou même en général, la formulation exacte consiste à savoir si elles peuvent être "supposées se terminer" ou non. Si une boucle ne peut pas se terminer, cela signifie que le code ultérieur n'est pas accessible, quels que soient les arguments que vous faites sur les mathématiques et les infinis et combien de temps il faut pour effectuer une quantité infinie de travail sur une machine hypothétique.
De plus, Clang n'est pas simplement une DeathStation 9000 conforme à la norme ISO C, il est destiné à être utile pour la programmation de systèmes de bas niveau dans le monde réel, y compris les noyaux et les éléments intégrés. Donc , si vous acceptiez ou non des arguments au sujet de C11 permettant la suppression de while(1);
, il ne fait pas de sens que Clang voudrait réellement faire cela. Si vous écrivez while(1);
, ce n'était probablement pas un accident. La suppression des boucles qui finissent par être infinies par accident (avec des expressions de contrôle de variable d'exécution) peut être utile, et il est logique que les compilateurs le fassent.
Il est rare que vous vouliez simplement tourner jusqu'à la prochaine interruption, mais si vous écrivez cela en C, c'est certainement ce que vous attendez. (Et ce qui se passe dans GCC et Clang, sauf pour Clang lorsque la boucle infinie est à l'intérieur d'une fonction wrapper).
Par exemple, dans un noyau de système d'exploitation primitif, lorsque le planificateur n'a aucune tâche à exécuter, il peut exécuter la tâche inactive. Une première implémentation de cela pourrait être while(1);
.
Ou pour le matériel sans aucune fonction d'économie d'énergie, cela pourrait être la seule implémentation. (Jusqu'au début des années 2000, c'était je pense pas rare sur x86. Bien que l' hlt
instruction existait, IDK si elle économisait une quantité significative d'énergie jusqu'à ce que les processeurs commencent à avoir des états de veille à faible puissance.)