Comment créer une boucle vide infinie qui ne sera pas optimisée?


131

La norme C11 semble impliquer que les instructions d'itération avec des expressions de contrôle constantes ne doivent pas être optimisées. Je prends mon conseil de cette réponse , qui cite spécifiquement la section 6.8.5 du projet de norme:

Une instruction d'itération dont l'expression de contrôle n'est pas une expression constante ... peut être supposée par l'implémentation se terminer.

Dans cette réponse, il mentionne qu'une boucle comme celle- while(1) ;ci ne devrait pas être soumise à l'optimisation.

Alors ... pourquoi Clang / LLVM optimise-t-il la boucle ci-dessous (compilée avec cc -O2 -std=c11 test.c -o test)?

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Sur ma machine, cela s'imprime begin, puis se bloque sur une instruction illégale (un ud2piège placé après die()). Sur godbolt , nous pouvons voir que rien n'est généré après l'appel à puts.

Cela a été une tâche étonnamment difficile d'obtenir que Clang produise une boucle infinie sous -O2- alors que je pouvais tester à plusieurs reprises une volatilevariable, ce qui implique une lecture en mémoire que je ne veux pas. Et si je fais quelque chose comme ça:

#include <stdio.h>

static void die() {
    while(1)
        ;
}

int main() {
    printf("begin\n");
    volatile int x = 1;
    if(x)
        die();
    printf("unreachable\n");
}

... Clang imprime beginsuivi unreachablecomme si la boucle infinie n'avait jamais existé.

Comment obtenir Clang pour produire une boucle infinie sans accès à la mémoire appropriée avec les optimisations activées?


3
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Bhargav Rao

2
Il n'y a pas de solution portable qui n'implique aucun effet secondaire. Si vous ne voulez pas avoir accès à la mémoire, votre meilleur espoir serait d'enregistrer un caractère volatil non signé; mais le registre disparaît en C ++ 17.
Scott M

25
Peut-être que ce n'est pas dans la portée de la question, mais je suis curieux de savoir pourquoi vous voulez faire cela. Il y a sûrement un autre moyen d'accomplir votre vraie tâche. Ou est-ce juste de nature académique?
Cruncher

1
@Cruncher: Les effets de toute tentative particulière d'exécution d'un programme peuvent être utiles, essentiellement inutiles ou bien pires qu'inutiles. Une exécution qui entraîne le blocage d'un programme dans une boucle sans fin peut être inutile, mais toujours préférable à d'autres comportements qu'un compilateur pourrait remplacer.
supercat

6
@Cruncher: Parce que le code peut s'exécuter dans un contexte autonome où il n'y a pas de concept exit(), et parce que le code peut avoir découvert une situation où il ne peut garantir que les effets d'une exécution continue ne seraient pas pires qu'inutiles . Une boucle de saut à soi est une façon assez moche de gérer de telles situations, mais elle peut néanmoins être la meilleure façon de gérer une mauvaise situation.
supercat

Réponses:


77

La norme C11 le dit, 6.8.5 / 6:

Une instruction d'itération dont l'expression de contrôle n'est pas une expression constante, 156) qui n'effectue aucune opération d'entrée / sortie, n'accède pas aux objets volatils et n'effectue aucune synchronisation ou opération atomique dans son corps, contrôlant l'expression ou (dans le cas d'un pour ), son expression-3, peut être supposée par l'implémentation se terminer. 157)

Les deux notes de bas de page ne sont pas normatives mais fournissent des informations utiles:

156) Une expression de contrôle omise est remplacée par une constante non nulle, qui est une expression constante.

157) Ceci est destiné à permettre des transformations du compilateur telles que la suppression de boucles vides même lorsque la terminaison ne peut pas être prouvée.

Dans votre cas, while(1)est une expression constante limpide, donc l'implémentation ne peut pas être considérée comme terminée. Une telle implémentation serait désespérément interrompue, car les boucles "pour toujours" sont une construction de programmation courante.

Ce qui arrive au "code inaccessible" après la boucle n'est cependant pas, à ma connaissance, bien défini. Cependant, le clang se comporte en effet très étrangement. Comparaison du code machine avec gcc (x86):

gcc 9.2 -O3 -std=c11 -pedantic-errors

.LC0:
        .string "begin"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
.L2:
        jmp     .L2

clang 9.0.0 -O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.Lstr:
        .asciz  "begin"

gcc génère la boucle, clang court juste dans les bois et se termine avec l'erreur 255.

Je penche pour que ce soit un comportement non conforme de clang. Parce que j'ai essayé de développer votre exemple comme ceci:

#include <stdio.h>
#include <setjmp.h>

static _Noreturn void die() {
    while(1)
        ;
}

int main(void) {
    jmp_buf buf;
    _Bool first = !setjmp(buf);

    printf("begin\n");
    if(first)
    {
      die();
      longjmp(buf, 1);
    }
    printf("unreachable\n");
}

J'ai ajouté C11 _Noreturnpour essayer d'aider le compilateur plus loin. Il doit être clair que cette fonction va raccrocher, à partir de ce seul mot-clé.

setjmprenverra 0 lors de la première exécution, donc ce programme devrait juste s'écraser sur le while(1)et s'arrêter là, n'imprimant que "begin" (en supposant que \ n vide la sortie standard). Cela se produit avec gcc.

Si la boucle a été simplement supprimée, elle devrait imprimer "commencer" 2 fois puis imprimer "inaccessible". Cependant, sur clang ( godbolt ), il imprime "begin" 1 fois puis "inaccessible" avant de retourner le code de sortie 0. C'est tout simplement faux, peu importe comment vous le mettez.

Je ne trouve aucun argument pour revendiquer un comportement indéfini ici, donc je pense que c'est un bogue dans Clang. En tout cas, ce comportement rend clang 100% inutile pour des programmes comme les systèmes embarqués, où vous devez simplement pouvoir compter sur des boucles éternelles suspendant le programme (en attendant un chien de garde, etc.).


15
Je ne suis pas d'accord sur "cette expression constante et limpide, donc la mise en œuvre ne peut pas supposer qu'elle se termine" . Cela pénètre vraiment dans la pratique juridique du langage, mais 6.8.5/6est sous la forme de si (ces) alors vous pouvez supposer (ceci) . Cela ne signifie pas sinon (ces) vous ne pouvez pas supposer (ceci) . C'est une spécification uniquement lorsque les conditions sont remplies, et non lorsqu'elles ne sont pas remplies, où vous pouvez faire tout ce que vous voulez en respectant les normes. Et s'il n'y a pas d'observables ...
kabanus

7
@kabanus La partie citée est un cas particulier. Sinon (le cas spécial), évaluez et séquencez le code comme vous le feriez normalement. Si vous continuez à lire le même chapitre, l'expression de contrôle est évaluée comme spécifié pour chaque instruction d'itération ("comme spécifié par la sémantique") à l'exception du cas spécial cité. Elle suit les mêmes règles que l'évaluation de tout calcul de valeur, qui est séquencée et bien définie.
Lundin

2
Je suis d'accord, mais vous ne seriez pas surpris que dans int z=3; int y=2; int x=1; printf("%d %d\n", x, z);il n'y a pas 2dans l'assemblage, donc dans le sens inutile vide xn'a pas été attribué après ymais après en zraison de l'optimisation. Donc, à partir de votre dernière phrase, nous suivons les règles habituelles, supposons que le temps soit arrêté (parce que nous n'étions pas mieux contraints), et laissé dans l'impression finale, "inaccessible". Maintenant, nous optimisons cette déclaration inutile (parce que nous ne savons pas mieux).
kabanus

2
@MSalters Un de mes commentaires a été supprimé, mais merci pour la contribution - et je suis d'accord. Ce que mon commentaire a dit, c'est que je pense que c'est le cœur du débat - c'est while(1);la même chose qu'une int y = 2;déclaration en termes de ce que la sémantique nous permet d'optimiser, même si leur logique reste à la source. À partir de n1528, j'avais l'impression qu'ils peuvent être les mêmes, mais comme les gens sont beaucoup plus expérimentés que moi, ils argumentent dans l'autre sens, et c'est apparemment un bug officiel, puis au-delà d'un débat philosophique sur la question de savoir si le libellé de la norme est explicite , l'argument est rendu théorique.
kabanus

2
«Une telle implémentation serait désespérément interrompue, car les boucles« pour toujours »sont une construction de programmation courante.» - Je comprends le sentiment mais l'argument est défectueux car il pourrait être appliqué de manière identique à C ++, mais un compilateur C ++ qui optimise cette boucle ne serait pas rompu mais conforme.
Konrad Rudolph

52

Vous devez insérer une expression susceptible de provoquer un effet secondaire.

La solution la plus simple:

static void die() {
    while(1)
       __asm("");
}

Lien Godbolt


21
N'explique cependant pas pourquoi Clang agit.
Lundin

4
Il suffit de dire "c'est un bug dans le clang". J'aimerais d'abord essayer quelques choses ici, avant de crier "bug".
Lundin

3
@Lundin Je ne sais pas si c'est un bug. La norme n'est pas techniquement précise dans ce cas
P__J__

4
Heureusement, GCC est open source et je peux écrire un compilateur qui optimise votre exemple. Et je pourrais le faire pour n'importe quel exemple que vous proposez, maintenant et à l'avenir.
Thomas Weller

3
@ThomasWeller: Les développeurs de GCC n'accepteraient pas un correctif qui optimise cette boucle; cela violerait le comportement documenté = garanti. Voir mon commentaire précédent: asm("")est implicitement asm volatile("");et donc l'instruction asm doit être exécutée autant de fois qu'elle le fait dans la machine abstraite gcc.gnu.org/onlinedocs/gcc/Basic-Asm.html . (Notez qu'il n'est pas sûr que ses effets secondaires incluent de la mémoire ou des registres; vous avez besoin d'Asm étendu avec un "memory"clobber si vous voulez lire ou écrire de la mémoire à laquelle vous accédez jamais depuis C.Asm de base n'est sûr que pour des choses comme asm("mfence")ou cli.)
Peter Cordes

50

D'autres réponses ont déjà couvert les moyens de faire émettre Clang la boucle infinie, avec le langage d'assemblage en ligne ou d'autres effets secondaires. Je veux juste confirmer qu'il s'agit bien d'un bug du compilateur. Plus précisément, il s'agit d' un bogue LLVM de longue date - il applique le concept C ++ de «toutes les boucles sans effets secondaires doivent se terminer» aux langages où il ne devrait pas, comme C.

Par exemple, le langage de programmation Rust autorise également des boucles infinies et utilise LLVM comme backend, et il a ce même problème.

À court terme, il semble que LLVM continuera de supposer que "toutes les boucles sans effets secondaires doivent se terminer". Pour tout langage autorisant des boucles infinies, LLVM s'attend à ce que le frontal insère des llvm.sideeffectopcodes dans ces boucles. C'est ce que Rust prévoit de faire, donc Clang (lors de la compilation du code C) devra probablement le faire aussi.


5
Rien de tel que l'odeur d'un bug qui date de plus d'une décennie ... avec plusieurs correctifs et correctifs proposés ... mais qui n'a toujours pas été corrigé.
Ian Kemp

4
@IanKemp: Pour qu'ils corrigent le bogue maintenant, il faudrait reconnaître qu'ils ont mis dix ans à le corriger. Mieux vaut tenir espoir que la Norme changera pour justifier leur comportement. Bien sûr, même si la norme changeait, cela ne justifierait toujours pas leur comportement, sauf aux yeux des personnes qui considéreraient la modification de la norme comme une indication que le mandat comportemental antérieur de la norme était un défaut qui devait être corrigé rétroactivement.
supercat

4
Il a été "corrigé" dans le sens où LLVM a ajouté l' sideeffectop (en 2017) et s'attend à ce que les frontaux insèrent cet op dans des boucles à leur discrétion. LLVM a dû choisir des valeurs par défaut pour les boucles, et il s'est avéré que celui-ci s'alignait sur le comportement de C ++, intentionnellement ou non. Bien sûr, il reste encore un certain travail d'optimisation à faire, par exemple pour fusionner les sideeffectopérations consécutives en une seule. (C'est ce qui empêche le frontal de Rust de l'utiliser.) Ainsi, sur cette base, le bogue se trouve dans le frontal (clang) qui n'insère pas l'op dans les boucles.
Arnavion

@Arnavion: Existe-t-il un moyen d'indiquer que les opérations peuvent être différées à moins ou jusqu'à ce que les résultats soient utilisés, mais que si les données provoquaient une boucle sans fin d'un programme, essayer de poursuivre les anciennes dépendances des données rendrait le programme pire qu'inutile ? Devoir ajouter des effets secondaires bidons qui empêcheraient les anciennes optimisations utiles pour empêcher l'optimiseur de rendre un programme pire qu'inutile ne semble pas être une recette pour l'efficacité.
supercat

Cette discussion appartient probablement aux listes de diffusion LLVM / clang. FWIW la validation LLVM qui a ajouté l'op a également enseigné plusieurs passes d'optimisation à ce sujet. De plus, Rust a expérimenté l'insertion d' sideeffectopérations au début de chaque fonction et n'a constaté aucune régression des performances d'exécution. Le seul problème est une régression de la compilation , apparemment en raison du manque de fusion des opérations consécutives comme je l'ai mentionné dans mon commentaire précédent.
Arnavion

32

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=gnu11vs -std=gnu99ne 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 asminstruction 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 mainqui appelle infloop() { while(1); }d'abord le même puts, mais arrête ensuite d'émettre des instructions pour mainaprè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 labelboucle 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 0partir 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' hltinstruction existait, IDK si elle économisait une quantité significative d'énergie jusqu'à ce que les processeurs commencent à avoir des états de veille à faible puissance.)


1
Par curiosité, quelqu'un utilise-t-il réellement clang pour les systèmes embarqués? Je ne l'ai jamais vu et je travaille exclusivement avec Embedded. gcc seulement "récemment" (il y a 10 ans) est entré dans le marché embarqué et je l'utilise avec scepticisme, de préférence avec de faibles optimisations et toujours avec -ffreestanding -fno-strict-aliasing. Cela fonctionne bien avec ARM et peut-être avec les AVR hérités.
Lundin

1
@Lundin: IDK sur l'embarqué, mais oui, les gens construisent des noyaux avec clang, au moins parfois Linux. Vraisemblablement aussi Darwin pour MacOS.
Peter Cordes

2
bugs.llvm.org/show_bug.cgi?id=965 ce bug semble pertinent, mais je ne suis pas sûr que ce soit ce que nous voyons ici.
bracco23

1
@lundin - Je suis presque sûr que nous avons utilisé GCC (et beaucoup d'autres boîtes à outils) pour le travail intégré tout au long des années 90, avec des RTOS comme VxWorks et PSOS. Je ne comprends pas pourquoi vous dites que GCC n'est entré sur le marché intégré que récemment.
Jeff Learman

1
@JeffLearman Est-il devenu grand public récemment, alors? Quoi qu'il en soit, le fiasco de gias strict d'alias ne s'est produit qu'après l'introduction de C99, et les versions plus récentes de celui-ci ne semblent plus devenir des bananes en cas de violations strictes d'alias. Pourtant, je reste sceptique chaque fois que je l'utilise. Quant à Clang, la dernière version est évidemment complètement cassée en ce qui concerne les boucles éternelles, elle ne peut donc pas être utilisée pour les systèmes embarqués.
Lundin

14

Juste pour mémoire, Clang se comporte également mal avec goto:

static void die() {
nasty:
    goto nasty;
}

int main() {
    int x; printf("begin\n");
    die();
    printf("unreachable\n");
}

Il produit le même résultat que dans la question, c'est-à-dire:

main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

Je ne vois aucun moyen de lire ceci comme autorisé dans C11, qui dit seulement:

6.8.6.1 (2) Une gotoinstruction provoque un saut inconditionnel à l'instruction préfixée par l'étiquette nommée dans la fonction englobante.

Comme goton'est pas une « déclaration d'itération » (6.8.5 listes while, doet for) rien indulgences appliquer « pris terminaison » spéciale, mais vous voulez les lire.

Le compilateur de liens Godbolt de la question d'origine est x86-64 Clang 9.0.0 et les drapeaux sont -g -o output.s -mllvm --x86-asm-syntax=intel -S --gcc-toolchain=/opt/compiler-explorer/gcc-9.2.0 -fcolor-diagnostics -fno-crash-diagnostics -O2 -std=c11 example.c

Avec d'autres tels que x86-64 GCC 9.2, vous obtenez le parfait:

.LC0:
  .string "begin"
main:
  sub rsp, 8
  mov edi, OFFSET FLAT:.LC0
  call puts
.L2:
  jmp .L2

Drapeaux: -g -o output.s -masm=intel -S -fdiagnostics-color=always -O2 -std=c11 example.c


Une implémentation conforme pourrait avoir une limite de traduction non documentée sur le temps d'exécution ou les cycles de CPU qui pourrait provoquer un comportement arbitraire si elle était dépassée, ou si un programme entrait un dépassement inévitable de la limite. Ces choses sont un problème de qualité de mise en œuvre, en dehors de la juridiction de la norme. Il semblerait étrange que les responsables de clang insistent tellement sur leur droit de produire une implémentation de mauvaise qualité, mais la norme le permet.
supercat

2
@supercat merci pour le commentaire ... pourquoi le dépassement d'une limite de traduction ferait autre chose que l'échec de la phase de traduction et refuser d'exécuter? En outre: " 5.1.1.3 Diagnostics Une implémentation conforme doit produire ... un message de diagnostic ... si une unité de traduction de prétraitement ou une unité de traduction contient une violation de toute règle ou contrainte de syntaxe ...". Je ne vois pas comment un comportement erroné lors de la phase d'exécution peut se conformer.
jonathanjo

La norme serait complètement impossible à implémenter si les limites d'implémentation devaient toutes être résolues au moment de la construction, car on pourrait écrire un programme strictement conforme qui nécessiterait plus d'octets de pile qu'il n'y a d'atomes dans l'univers. Il n'est pas clair si les limitations d'exécution doivent être regroupées avec des "limites de traduction", mais une telle concession est clairement nécessaire, et il n'y a aucune autre catégorie dans laquelle elle pourrait être placée.
supercat

1
Je répondais à votre commentaire sur les "limites de traduction". Bien sûr, il y a aussi des limites d'exécution, j'avoue que je ne comprends pas pourquoi vous suggérez qu'elles devraient être regroupées avec des limites de traduction ou pourquoi vous dites que c'est nécessaire. Je ne vois tout simplement aucune raison de dire que nasty: goto nastypeut être conforme et ne pas faire tourner le (s) CPU jusqu'à ce que l'épuisement de l'utilisateur ou des ressources intervienne.
jonathanjo

1
Le Standard ne fait aucune référence aux "limites d'exécution" que j'ai pu trouver. Des choses comme l'imbrication des appels de fonction sont généralement gérées par l'allocation de pile, mais une implémentation conforme qui limite les appels de fonction à une profondeur de 16 pourrait créer 16 copies de chaque fonction et avoir un appel à l' bar()intérieur foo()traité comme un appel de __1fooà __2bar, de __2fooà __3bar, etc. et de __16fooà __launch_nasal_demons, ce qui permettrait alors à tous les objets automatiques d'être alloués statiquement, et ferait ce qui est généralement une limite "d'exécution" en une limite de traduction.
supercat

5

Je jouerai l'avocat du diable et soutiendrai que la norme n'interdit pas explicitement à un compilateur d'optimiser une boucle infinie.

Une instruction d'itération dont l'expression de contrôle n'est pas une expression constante, 156) qui n'effectue aucune opération d'entrée / sortie, n'accède pas aux objets volatils et n'effectue aucune synchronisation ou opération atomique dans son corps, contrôlant l'expression ou (dans le cas d'un pour ), son expression-3, peut être supposée par l'implémentation se terminer.157)

Analysons cela. Une instruction d'itération qui satisfait certains critères peut être supposée se terminer:

if (satisfiesCriteriaForTerminatingEh(a_loop)) 
    if (whatever_reason_or_just_because_you_feel_like_it)
         assumeTerminates(a_loop);

Cela ne dit rien sur ce qui se passe si les critères ne sont pas satisfaits et supposer qu'une boucle peut se terminer même alors n'est pas explicitement interdit tant que d'autres règles de la norme sont respectées.

do { } while(0)ou while(0){}sont après tout des instructions d'itération (boucles) qui ne satisfont pas aux critères qui permettent à un compilateur de simplement supposer sur un coup de tête qu'elles se terminent et pourtant elles se terminent évidemment.

Mais le compilateur peut-il tout simplement optimiser while(1){}?

5.1.2.3p4 dit:

Dans la machine abstraite, toutes les expressions sont évaluées comme spécifié par la sémantique. Une implémentation réelle n'a pas besoin d'évaluer une partie d'une expression si elle peut en déduire que sa valeur n'est pas utilisée et qu'aucun effet secondaire nécessaire n'est produit (y compris ceux provoqués par l'appel d'une fonction ou l'accès à un objet volatil).

Cela mentionne des expressions, pas des déclarations, donc ce n'est pas convaincant à 100%, mais cela permet certainement des appels comme:

void loop(void){ loop(); }

int main()
{
    loop();
}

à sauter. Fait intéressant, clang le saute, et gcc ne le fait pas .


"Cela ne dit rien sur ce qui se passe si les critères ne sont pas satisfaits" Mais c'est le cas, 6.8.5.1 L'instruction while: "L'évaluation de l'expression de contrôle a lieu avant chaque exécution du corps de la boucle." C'est ça. Il s'agit d'un calcul de valeur (d'une expression constante), il relève de la règle de la machine abstraite 5.1.2.3 qui définit le terme évaluation: "L' évaluation d'une expression comprend en général à la fois des calculs de valeur et l'initiation d'effets secondaires." Et selon le même chapitre, toutes ces évaluations sont séquencées et évaluées comme spécifié par la sémantique.
Lundin

1
@Lundin Il while(1){}y a donc une séquence infinie d' 1évaluations entrelacées avec des {}évaluations, mais où dans la norme dit-il que ces évaluations doivent prendre un temps différent de zéro ? Le comportement de gcc est plus utile, je suppose, car vous n'avez pas besoin de trucs impliquant un accès à la mémoire ou des trucs en dehors du langage. Mais je ne suis pas convaincu que la norme interdit cette optimisation en clang. Si rendre non while(1){}optimisable est l'intention, la norme devrait être explicite à ce sujet et le bouclage infini devrait être répertorié comme un effet secondaire observable dans 5.1.2.3p2.
PSkocik

1
Je pense que c'est spécifié, si vous traitez la 1condition comme un calcul de valeur. Le temps d'exécution n'a pas d'importance - ce qui compte, c'est ce qui while(A){} B;peut ne pas être entièrement optimisé, pas optimisé B;et non re-séquencé B; while(A){}. Pour citer la machine abstraite C11, je souligne: "La présence d'un point de séquence entre l'évaluation des expressions A et B implique que chaque calcul de valeur et effet secondaire associé à A est séquencé avant chaque calcul de valeur et effet secondaire associé à B ". La valeur de Aest clairement utilisée (par la boucle).
Lundin

2
+1 Même s'il me semble que "l'exécution se bloque indéfiniment sans aucune sortie" est un "effet secondaire" dans toute définition de "effet secondaire" qui a du sens et est utile au-delà de la norme dans le vide, cela aide à expliquer l'état d'esprit à partir duquel cela peut avoir un sens pour quelqu'un.
mtraceur

1
Près de "optimiser une boucle infinie" : il n'est pas tout à fait clair si "cela" se réfère à la norme ou au compilateur - peut-être reformuler? Étant donné «bien que cela devrait probablement» et non «bien que cela ne devrait probablement pas» , c'est probablement la norme à laquelle «il» fait référence.
Peter Mortensen

2

J'ai été convaincu que ce n'est qu'un simple vieux bug. Je laisse mes tests ci-dessous et en particulier la référence à la discussion au sein du comité standard pour certains raisonnements que j'avais précédemment.


Je pense que c'est un comportement indéfini (voir fin), et Clang n'a qu'une seule implémentation. GCC fonctionne en effet comme vous vous y attendez, optimisant uniquement l' unreachableinstruction print mais laissant la boucle. Certains comment Clang prend étrangement des décisions en combinant la doublure et en déterminant ce qu'elle peut faire avec la boucle.

Le comportement est très étrange - il supprime l'impression finale, donc "voir" la boucle infinie, mais aussi se débarrasser de la boucle.

C'est encore pire pour autant que je sache. Suppression de l'inline que nous obtenons:

die: # @die
.LBB0_1: # =>This Inner Loop Header: Depth=1
  jmp .LBB0_1
main: # @main
  push rax
  mov edi, offset .Lstr
  call puts
.Lstr:
  .asciz "begin"

de sorte que la fonction est créée et l'appel optimisé. C'est encore plus résistant que prévu:

#include <stdio.h>

void die(int x) {
    while(x);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

entraîne un assemblage très non optimal pour la fonction, mais l'appel de fonction est à nouveau optimisé! Encore pire:

void die(x) {
    while(x++);
}

int main() {
    printf("begin\n");
    die(1);
    printf("unreachable\n");
}

J'ai fait un tas d'autres tests en ajoutant une variable locale et en l'augmentant, en passant un pointeur, en utilisant un gotoetc ... À ce stade, j'abandonnerais. Si vous devez utiliser clang

static void die() {
    int volatile x = 1;
    while(x);
}

Fait le travail. Il aspire à l'optimisation (évidemment), et part en finale redondante printf. Au moins, le programme ne s'arrête pas. Peut-être GCC après tout?

Addenda

Après discussion avec David, je cède que la norme ne dit pas "si la condition est constante, vous ne pouvez pas supposer que la boucle se termine". En tant que tel, et conformément à la norme, il n'y a pas de comportement observable (tel que défini dans la norme), je ne plaiderais que pour la cohérence - si un compilateur optimise une boucle parce qu'il suppose qu'elle se termine, il ne doit pas optimiser les instructions suivantes.

Heck n1528 a ces comportements indéfinis si je lis bien. Plus précisément

Un problème majeur pour cela est qu'il permet au code de se déplacer à travers une boucle potentiellement non terminante

À partir de là, je pense que cela ne peut se résumer qu'à une discussion de ce que nous voulons (attendu?) Plutôt que de ce qui est autorisé.


Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Bhargav Rao

Re "plain all bug" : Voulez-vous dire " plain old bug" ?
Peter Mortensen

@PeterMortensen "ole" me conviendrait aussi.
kabanus

2

Il semble que ce soit un bug dans le compilateur Clang. S'il n'y a aucune contrainte sur la die()fonction d'être une fonction statique, supprimez-la staticet faites-la inline:

#include <stdio.h>

inline void die(void) {
    while(1)
        ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

Il fonctionne comme prévu lorsqu'il est compilé avec le compilateur Clang et est également portable.

Explorateur du compilateur (godbolt.org) - clang 9.0.0-O3 -std=c11 -pedantic-errors

main:                                   # @main
        push    rax
        mov     edi, offset .Lstr
        call    puts
.LBB0_1:                                # =>This Inner Loop Header: Depth=1
        jmp     .LBB0_1
.Lstr:
        .asciz  "begin"

Et alors static inline?
SS Anne

1

Les éléments suivants semblent fonctionner pour moi:

#include <stdio.h>

__attribute__ ((optnone))
static void die(void) {
    while (1) ;
}

int main(void) {
    printf("begin\n");
    die();
    printf("unreachable\n");
}

à godbolt

Dire explicitement à Clang de ne pas optimiser qu'une fonction entraîne l'émission d'une boucle infinie comme prévu. Espérons qu'il existe un moyen de désactiver sélectivement certaines optimisations au lieu de simplement les désactiver toutes comme ça. Clang refuse toujours d'émettre du code pour le second printf, cependant. Pour le forcer à le faire, j'ai dû modifier davantage le code à l'intérieur mainpour:

volatile int x = 0;
if (x == 0)
    die();

Il semble que vous deviez désactiver les optimisations pour votre fonction de boucle infinie, puis vous assurer que votre boucle infinie est appelée conditionnellement. Dans le monde réel, ce dernier est presque toujours le cas de toute façon.


1
Il n'est pas nécessaire que la seconde printfsoit générée si la boucle va réellement pour toujours, car dans ce cas, la seconde printfest vraiment inaccessible et peut donc être supprimée. (L'erreur de Clang est à la fois de détecter l'inaccessibilité et de supprimer la boucle de sorte que le code inaccessible soit atteint).
nneonneo

Documents du CCG __attribute__ ((optimize(1))), mais clang l'ignore comme non pris en charge: godbolt.org/z/4ba2HM . gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html
Peter Cordes

0

Une implémentation conforme peut, et de nombreuses applications pratiques le font, imposer des limites arbitraires sur la durée d'exécution d'un programme ou sur le nombre d'instructions qu'il exécuterait, et se comporter de manière arbitraire si ces limites sont violées ou - selon la règle "comme si" - s'il détermine qu'ils seront inévitablement violés. À condition qu'une implémentation puisse traiter avec succès au moins un programme qui exerce nominalement toutes les limites énumérées dans N1570 5.2.4.1 sans atteindre aucune limite de traduction, l'existence de limites, la mesure dans laquelle elles sont documentées et les effets de leur dépassement sont tous les problèmes de qualité de mise en œuvre en dehors de la juridiction de la norme.

Je pense que l'intention de la norme est tout à fait claire que les compilateurs ne devraient pas supposer qu'une while(1) {}boucle sans effets secondaires ni breakinstructions se terminera. Contrairement à ce que certains pourraient penser, les auteurs de la norme n'invitaient pas les rédacteurs de compilateurs à être stupides ou obtus. Une implémentation conforme pourrait utilement décider de mettre fin à tout programme qui, s'il n'était pas interrompu, exécuterait plus d'instructions libres d'effets secondaires qu'il n'y a d'atomes dans l'univers, mais une implémentation de qualité ne devrait pas effectuer une telle action sur la base d'une hypothèse concernant résiliation mais plutôt sur la base que cela pourrait être utile, et ne serait pas (contrairement au comportement de clang) pire qu'inutile.


-2

La boucle n'a pas d'effets secondaires et peut donc être optimisée. La boucle est en fait un nombre infini d'itérations de zéro unité de travail. Ceci n'est pas défini en mathématiques et en logique et la norme ne dit pas si une implémentation est autorisée à accomplir un nombre infini de choses si chaque chose peut être faite en un temps nul. L'interprétation de Clang est parfaitement raisonnable en traitant l'infini fois zéro comme zéro plutôt que l'infini. La norme ne dit pas si une boucle infinie peut se terminer si tout le travail dans les boucles est en fait terminé.

Le compilateur est autorisé à optimiser tout ce qui n'est pas un comportement observable tel que défini dans la norme. Cela comprend le temps d'exécution. Il n'est pas nécessaire de conserver le fait que la boucle, si elle n'est pas optimisée, prendrait un temps infini. Il est permis de changer cela en un temps d'exécution beaucoup plus court - en fait, c'est le point de la plupart des optimisations. Votre boucle a été optimisée.

Même si clang a traduit le code naïvement, vous pourriez imaginer un processeur d'optimisation capable de terminer chaque itération en deux fois moins de temps que l'itération précédente. Cela compléterait littéralement la boucle infinie en un temps fini. Un tel processeur optimisant viole-t-il la norme? Il semble assez absurde de dire qu'un processeur d'optimisation violerait la norme s'il est trop bon pour l'optimisation. Il en va de même pour un compilateur.


Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew

4
À en juger par l'expérience que vous avez (de votre profil), je ne peux que conclure que ce message est écrit de mauvaise foi juste pour défendre le compilateur. Vous dites sérieusement que quelque chose qui prend un temps infini peut être optimisé pour s'exécuter en deux fois moins de temps. C'est ridicule à tous les niveaux et vous le savez.
pipe

@pipe: Je pense que les responsables de clang et gcc espèrent qu'une future version de la norme rendra le comportement de leurs compilateurs admissible, et les responsables de ces compilateurs pourront prétendre qu'un tel changement n'était qu'une correction d'un défaut de longue date dans la norme. C'est ainsi qu'ils ont traité les garanties de séquence initiale commune de C89, par exemple.
supercat

@SSAnne: Hmm ... Je ne pense pas que ce soit suffisant pour bloquer certaines des inférences saines que gcc et clang tirent des résultats des comparaisons d'égalité de pointeurs.
supercat

@supercat Il y a <s> autres </s> tonnes.
SS Anne

-2

Je suis désolé si ce n'est absurdement pas le cas, je suis tombé sur ce post et je sais parce que mes années d'utilisation de la distribution Gentoo Linux que si vous voulez que le compilateur n'optimise pas votre code, vous devez utiliser -O0 (Zero). J'étais curieux à ce sujet, et j'ai compilé et exécuté le code ci-dessus, et la boucle va indéfiniment. Compilé avec clang-9:

cc -O0 -std=c11 test.c -o test

1
Le but est de faire une boucle infinie avec les optimisations activées.
SS Anne

-4

Une whileboucle vide n'a aucun effet secondaire sur le système.

Par conséquent, Clang le supprime. Il existe de "meilleures" façons de réaliser le comportement prévu qui vous obligent à être plus évident de vos intentions.

while(1); est baaadd.


6
Dans de nombreuses constructions intégrées, il n'y a pas de concept de abort()ou exit(). Si une situation survient lorsqu'une fonction détermine que (peut-être à la suite d'une corruption de la mémoire) une exécution continue serait pire que dangereuse, un comportement par défaut courant pour les bibliothèques intégrées consiste à appeler une fonction qui exécute un while(1);. Il peut être utile pour le compilateur d'avoir des options pour remplacer un comportement plus utile , mais tout rédacteur de compilateur qui ne peut pas comprendre comment traiter une construction aussi simple comme une barrière à l'exécution continue du programme est incompétent pour faire confiance à des optimisations complexes.
supercat

Y a-t-il un moyen pour vous d'être plus explicite sur vos intentions? l'optimiseur est là pour optimiser votre programme, et une suppression des boucles redondantes qui ne font rien EST une optimisation. c'est vraiment une différence philosophique entre la pensée abstraite du monde des mathématiques et le monde de l'ingénierie plus appliquée.
Jameis célèbre

La plupart des programmes ont un ensemble d'actions utiles qu'ils doivent effectuer lorsque cela est possible, et un ensemble d'actions pires qu'inutiles qu'ils ne doivent en aucun cas effectuer. De nombreux programmes ont un ensemble de comportements acceptables dans un cas particulier, dont l'un, si le temps d'exécution n'est pas observable, serait toujours "attendre arbitrairement puis exécuter une action à partir de l'ensemble". Si toutes les actions autres que l'attente sont dans l'ensemble des actions pires qu'inutiles, il n'y aurait pas de nombre de secondes N pour lesquelles "attendre pour toujours" serait
sensiblement

... "attendez N + 1 secondes puis effectuez une autre action", de sorte que le fait que l'ensemble des actions tolérables autres que l'attente soit vide ne soit pas observable. D'un autre côté, si un morceau de code supprime une action intolérable de l'ensemble des actions possibles, et qu'une de ces actions est exécutée de toute façon , cela devrait être considéré comme observable. Malheureusement, les règles de langage C et C ++ utilisent le mot «assumer» d'une manière étrange, contrairement à tout autre domaine de la logique ou de l'effort humain que je peux identifier.
supercat

1
@FamousJame est ok, mais Clang ne se contente pas de supprimer la boucle - il analyse statiquement tout ce qui est ensuite inaccessible et émet une instruction invalide. Ce n'est pas ce à quoi vous vous attendez s'il "supprime" simplement la boucle.
nneonneo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.