Boucle infinie en C : for ou while ?

On s’est déjà posé cette question, on a déjà vu ou entendu des gens la poser : faut-il faire une boucle infinie avec while (1) ou avec for( ; ; ) en C ?

La logique veut que le résultat soit le même. J’ai donc fait le test avec le compilateur MinGW sous Windows 7 64 bits. J’ai pour cela écrit deux fonctions placées dans le fichier boucles.c :

void withFor(void)
{
    for(;;)
        ;
}

void withWhile(void)
{
    while(1)
        ;
}

J’ai ensuite utilisé objdump pour déassembler le code :

PS D:\Users\pgradot\Documents\C\out\Debug> objdump.exe -d .\boucles.o
.\boucles.o:     file format pe-i386

Disassembly of section .text:

00000000 :
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   eb fe                   jmp    3 

00000005 :
   5:   55                      push   %ebp
   6:   89 e5                   mov    %esp,%ebp
   8:   eb fe                   jmp    8 
   a:   90                      nop
   b:   90                      nop

Les 2 NOP à la fin m’ont étonné. J’ai inversé les positions des fonctions dans le fichier et j’ai alors constaté que les 2 NOP étaient toujours à la fin du fichier déassemblé. Pour m’assurer qu’ils ne faisaient effectivement pas partie des fonctions (vu les JMP, c’était quasiment certain), j’ai rajouté une fonction à la fin du fichier :

void withWhile(void)
{
    while(1)
        ;
}

void withFor(void)
{
    for(;;)
        ;
}

void nop()
{

}

Voici le résultat en assembleur :

PS D:\Users\pgradot\Documents\C\out\Debug> objdump.exe -d .\boucles.o

.\boucles.o:     file format pe-i386

Disassembly of section .text:

00000000 :
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   eb fe                   jmp    3 

00000005 :
   5:   55                      push   %ebp
   6:   89 e5                   mov    %esp,%ebp
   8:   eb fe                   jmp    8 

0000000a :
   a:   55                      push   %ebp
   b:   89 e5                   mov    %esp,%ebp
   d:   5d                      pop    %ebp
   e:   c3                      ret
   f:   90                      nop

Il n’y a donc aucune différence de performance entre les deux boucles, en tout cas avec ce compilateur et cette cible. J’ai des résultats similaires avec llvm/gcc et otool sous Mac OS X. Il en y a qui disent que for( ; ; ) donnerait de meilleures performances que while (1) car il y a une évaluation de condition dans la deuxième écriture. A l’évidence, mon compilateur ne se laisse pas avoir. Ce n’était peut-être pas le cas avec des compilateurs anciens mais je pense qu’on peut dire que cela relève maintenant des mythes du passé.

Il peut enfin rester des considérations sémantiques : laquelle des deux écritures représentent mieux l’idée de boucle infinie ? Je pense que cette discussion de stackoverflow les résume bien. En particulier, j’aime beaucoup l’écriture suivante qui y est proposée et donc la sémantique est parfaite ^^ :

#define EVER ;;

void forever(void)
{
        for(EVER)
                ;
}

On peut regarder le code en sortie du pré-processeur :

gcc -E -P boucles.c 

void forever(void)
{
 for(;;)
  ;
}

Et enfin l’assembleur généré :

otool -tv boucles.o 
boucles.o:
(__TEXT,__text) section
_forever:
0000000000000000	pushq	%rbp
0000000000000001	movq	%rsp,%rbp
0000000000000004	jmp	0x00000004

Avis personnel : je met toujours while (1) (ou while (true) en Java). C’est pour moi l’écriture la plus claire à lire et surtout la plus facile à dire.
« Hey machin, j’ai fait une boucle for(;;) !
– Gné ?!
– Oui, un while un quoi !
– Ahhhhh ! »

Publicités

8 Réponses

  1. Yoann

    +1 pour while(1) à cause de la sémantique (l’argument en faveur de « for(;;) » étant qu’il fait un caractère de moins)

    Je mets while(42) dans mon code perso 🙂

    J'aime

    25 septembre 2013 à 8:52

  2. Il y a plein d’autres façons de faire une boucle infinie:

    – Avec goto
    – Avec un do/while (et pour faire plus joli, avec #define while FOREVER pour écrire do { … } FOREVER)
    – Avec une récursivité loop() { … loop() }; (en -O2 gcc peut optimiser ça pour faire le même code que dans les autres cas)

    Tu devrais faire tes tests en -O0 pour vérifier que le code généré est bien équivalent. Dans ce cas précis il n’y a pas de différence (enfon sauf pour la version récursive, mais il ne faut pas faire ça!), mais parfois si.

    Et puis, ce n’est pas parce que gcc peut optimiser le code qu’il faut écrire n’importe quoi. gcc pousse assez loin, je dirai même de façon inquiétante, certaines optimisations. Par exemple, printf(« une chaine constante\n »); est remplacé par puts(« une chaine constante »);. Un compilateur C n’est pas sensé savoir que les deux sont équivalents…

    Entre for(;;) et while(true), la différence est en fait sémantique. Le premier signifie « il n’y a pas de condition d’arrêt », le deuxième signifie « la condition d’arrêt n’est jamais satisfaite ». Les deux sont équivalents au final, mais je trouve plus simple et directe la première formulation. Et le compilateur, même s’il peut comprendre les deux, sera d’accord avec moi.

    Enfin, quand on veut optimiser, il vaut mieux chercher les for(int i = 0; i < strlen(chaine); i++) qui font un appel à strlen à chaque itération. ça c'est du vrai temps perdu pour rien !

    J'aime

    10 octobre 2013 à 12:58

  3. Ah ! Monsieur a retrouvé Internet 🙂

    J’avoue que je ne sais pas quel est le niveau d’optimisation dans ces tests. Ceux avec MinGW ont été fait dans un projet CodeBlocks dont je n’ai pas vérifié les options…..(je regarde)…. je n’avais pas mis d’option. Donc comme ceux avec llvm, c’est le niveau d’optimisation par défaut. En regardant cette page, cela semble être -O0 : http://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html Je verrai un jour si j’y pense ^^

    Pourquoi trouves-tu cela inquiétant ? N’est-ce pas aussi ce qu’on demande à un compilateur ? Tant que cela ne modifie pas le comportement attendu, pourquoi s’en priver ?

    Je suis d’accord sur la différence sémantique. C’est un peu à chacun de choisir sa façon préférée 🙂 D’ailleurs, je me disais qu’un petit :
    #define END_OF_UNIVERSE_NOT_REACHED 1
    while(END_OF_UNIVERSE_NOT_REACHED) { /* … */ }
    pourrait être pas mal ^^

    J'aime

    11 octobre 2013 à 8:15

  4. Ah oui tiens, je pensais que le défaut était -O. Au temps pour moi.
    L’optimisation du printf en puts est inquiétante car on sort du C (le langage) pour entrer dans la lib standard. Hereusement, cette optimisation n’est pas faite quand on compile en mode freestanding, car le printf et le puts ne sont plus forcément ce qu’on croit dans ce cas. Il reste à espérer que gcc ne s’embrouille pas si un petit malin redéfinit l’une des deux fonctions (printf ou puts) pour faire autre chose…

    Sinon, n’oublions pas le fameux appel système très pratique dans ce cas qui est disponible dans Haiku: is_computer_on(). Voir ici:
    http://www.haiku-os.org/legacy-docs/bebook/TheKernelKit_SystemInfo.html

    (attention, c’est un appel système, c’est donc plus lent qu’un define!)
    En tout cas, un define IS_COMPUTER_ON sera sans doute plus réaliste.

    J'aime

    11 octobre 2013 à 10:08

    • J’ai entendu parlé pour la première fois de hosted et freestanding ces jours-ci, il faudrait que je regarde ce que ça veut dire…

      Quand tu dis « remplace », c’est vraiment un remplacement ou c’est « génère le même code » ?

      J’ai beaucoup les 2 dernières fonctions dans ton lien XD

      J'aime

      11 octobre 2013 à 5:52

  5. Le mode hosted, c’est quand il y a un OS en dessous de ton programme et une libc disponible. L’exécution commence par la fonction int main(int argc, char* argv[]), les handlers atexit sont appelés à la fin, tu peux faire des malloc et des free, etc. En mode freestanding, on oublie tout ça. C’est le mode qu’on va utiliser pour écrire un noyau de système par exemple. Le point d’entrée n’est pas défini, à toi de te débrouiller. On va garder stdint, stdbool, et ce genre de trucs, mais rien d’autre. Le compilateur ne peut donc plus se permettre de remplacer les strlen, strcpy, et autres trucs de ce genre comme gcc le fait (sauf avec -fno-builtins).

    Quand je dis remplace, c’est remplace. Exemple:

    ~> cat main.c
    #include
    int main(int argc, char** argv)
    {
    printf(« Hello, world\n »);
    }
    ~> /system/develop/tools/x86/bin/gcc -c main.c -o main.o
    ~> readelf -sW main.o

    Symbol table ‘.symtab’ contains 13 entries:
    Num: Value Size Type Bind Vis Ndx Name
    0: 00000000 0 NOTYPE LOCAL DEFAULT UND
    1: 00000000 0 FILE LOCAL DEFAULT ABS main.c
    2: 00000000 0 SECTION LOCAL DEFAULT 2
    3: 00000000 0 SECTION LOCAL DEFAULT 4
    4: 00000000 0 SECTION LOCAL DEFAULT 5
    5: 00000000 0 SECTION LOCAL DEFAULT 6
    6: 00000000 0 SECTION LOCAL DEFAULT 7
    7: 00000000 0 SECTION LOCAL DEFAULT 8
    8: 00000000 0 SECTION LOCAL DEFAULT 1
    9: 00000000 54 FUNC GLOBAL DEFAULT 2 main
    10: 00000000 0 FUNC GLOBAL HIDDEN 7 __x86.get_pc_thunk.bx
    11: 00000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
    12: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts

    Surprise, pas de printf dans la table des symboles! Et le \n à la fin de la chaîne a mystérieusement disparu! Et ce, même en -O0… J’ai quand même envie de dire que ce n’est pas ce que j’ai demandé…

    J'aime

    11 octobre 2013 à 6:19

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s