web-dev-qa-db-fra.com

Les compilateurs produisent-ils un meilleur code pour les boucles do-while par rapport aux autres types de boucles?

Il y a un commentaire dans la bibliothèque de compression zlib (qui est utilisé dans le projet Chromium parmi beaucoup d'autres) qui implique qu'une boucle do-while en C génère un "meilleur" code sur la plupart des compilateurs. Voici l'extrait de code où il apparaît.

do {
} while (*(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         *(ushf*)(scan+=2) == *(ushf*)(match+=2) &&
         scan < strend);
/* The funny "do {}" generates better code on most compilers */

https://code.google.com/p/chromium/codesearch#chromium/src/third_party/zlib/deflate.c&l=1225

Existe-t-il des preuves que la plupart (ou n'importe quel) compilateur générerait un meilleur code (par exemple plus efficace)?

Mise à jour: Mark Adler , l'un des auteurs originaux, a donné un peu de contexte dans le commentaires.

89
Dennis

Tout d'abord:

UNE do-while boucle n'est pas la même chose qu'une boucle while- ou une boucle for-.

  • Les boucles while et for peuvent ne pas exécuter du tout le corps de la boucle.
  • UNE do-while loop exécute toujours le corps de la boucle au moins une fois - il ignore la vérification de condition initiale.

Voilà donc la différence logique. Cela dit, tout le monde n'y adhère pas strictement. Il est assez courant que les boucles while ou for soient utilisées même s'il est garanti qu'elles boucleront toujours au moins une fois. (Surtout dans les langues avec foreach boucles.)

Donc, pour éviter de comparer des pommes et des oranges, je vais continuer en supposant que la boucle fonctionnera toujours au moins une fois. De plus, je ne mentionnerai pas les boucles for car ce sont essentiellement des boucles while avec un peu de sucre de syntaxe pour un compteur de boucles.

Je vais donc répondre à la question:

Si une boucle while est garantie de boucler au moins une fois, y a-t-il un gain de performances en utilisant un do-while boucle à la place.


UNE do-while ignore la première vérification de condition. Il y a donc une branche de moins et une condition de moins à évaluer.

Si la condition est coûteuse à vérifier et que vous savez que vous êtes assuré de boucler au moins une fois, un do-while la boucle pourrait être plus rapide.

Et bien que cela soit au mieux considéré comme une micro-optimisation, c'est une compilations que le compilateur ne peut pas toujours faire: en particulier lorsque le compilateur n'est pas en mesure de prouver que la boucle entrera toujours au moins une fois.


En d'autres termes, une boucle while:

while (condition){
    body
}

Est effectivement le même que celui-ci:

if (condition){
    do{
        body
    }while (condition);
}

Si vous savez que vous bouclerez toujours au moins une fois, cette instruction if est superflue.


De même au niveau de l'assemblage, c'est à peu près comment les différentes boucles se compilent pour:

boucle do-while:

start:
    body
    test
    conditional jump to start

boucle while:

    test
    conditional jump to end
start:
    body
    test
    conditional jump to start
end:

Notez que la condition a été dupliquée. Une autre approche consiste à:

    unconditional jump to end
start:
    body
end:
    test
    conditional jump to start

... qui échange le code en double pour un saut supplémentaire.

Quoi qu'il en soit, c'est encore pire qu'une normale do-while boucle.

Cela dit, les compilateurs peuvent faire ce qu'ils veulent. Et s'ils peuvent prouver que la boucle entre toujours une fois, alors elle a fait le travail pour vous.


Mais les choses sont un peu étranges pour l'exemple particulier de la question car il a un corps de boucle vide. Puisqu'il n'y a pas de corps, il n'y a pas de différence logique entre while et do-while.

FWIW, j'ai testé cela dans Visual Studio 2012:

  • Avec le corps vide, il génère en fait le même code pour while et do-while. Donc, cette partie est probablement un vestige de l'ancien temps où les compilateurs n'étaient pas aussi grands.

  • Mais avec un corps non vide, VS2012 parvient à éviter la duplication du code de condition, mais génère toujours un saut conditionnel supplémentaire.

Il est donc ironique que, tandis que l'exemple de la question souligne pourquoi un do-while la boucle pourrait être plus rapide dans le cas général, l'exemple lui-même ne semble pas donner d'avantage sur un compilateur moderne.

Compte tenu de l'âge du commentaire, nous ne pouvons que deviner pourquoi il importerait. Il est très possible que les compilateurs de l'époque n'étaient pas capables de reconnaître que le corps était vide. (Ou s'ils l'ont fait, ils n'ont pas utilisé les informations.)

108
Mysticial

Existe-t-il des preuves que la plupart (ou n'importe quel) compilateur générerait un meilleur code (par exemple plus efficace)?

Pas grand-chose, sauf si vous regardez l'assemblage généré par réel d'un compilateur réel et spécifique sur une plate-forme spécifique avec un peu optimisation spécifique paramètres.

Cela valait probablement la peine de s'inquiéter il y a des décennies (lorsque ZLib a été écrit), mais certainement pas de nos jours, sauf si vous avez trouvé, par profilage réel que cela supprime un goulot d'étranglement de votre code.

24
user529758

En un mot (tl; dr):

J'interprète le commentaire dans le code des OP un peu différemment, je pense que le "meilleur code" qu'ils prétendent avoir observé était dû au déplacement du travail réel dans la "condition" de la boucle. Je suis tout à fait d'accord cependant, c'est très spécifique au compilateur et que la comparaison qu'ils ont faite, tout en étant capable de produire un code légèrement différent, est pour la plupart inutile et probablement obsolète, comme je le montre ci-dessous.


Détails:

Il est difficile de dire ce que l'auteur d'origine voulait dire par son commentaire sur ce do {} while produisant un meilleur code, mais je voudrais spéculer dans une autre direction que ce qui a été soulevé ici - nous pensons que la différence entre do {} while et while {} les boucles sont assez minces (une branche en moins comme Mystical l'a dit), mais il y a quelque chose d'encore plus "drôle" dans ce code et qui met tout le travail à l'intérieur de cette folle condition, et garde la partie interne vide (do {}).

J'ai essayé le code suivant sur gcc 4.8.1 (-O3), et cela donne une différence intéressante -

#include "stdio.h" 
int main (){
    char buf[10];
    char *str = "hello";
    char *src = str, *dst = buf;

    char res;
    do {                            // loop 1
        res = (*dst++ = *src++);
    } while (res);
    printf ("%s\n", buf);

    src = str;
    dst = buf;
    do {                            // loop 2
    } while (*dst++ = *src++);
    printf ("%s\n", buf);

    return 0; 
}

Après la compilation -

00000000004003f0 <main>:
  ... 
; loop 1  
  400400:       48 89 ce                mov    %rcx,%rsi
  400403:       48 83 c0 01             add    $0x1,%rax
  400407:       0f b6 50 ff             movzbl 0xffffffffffffffff(%rax),%edx
  40040b:       48 8d 4e 01             lea    0x1(%rsi),%rcx
  40040f:       84 d2                   test   %dl,%dl
  400411:       88 16                   mov    %dl,(%rsi)
  400413:       75 eb                   jne    400400 <main+0x10>
  ...
;loop 2
  400430:       48 83 c0 01             add    $0x1,%rax
  400434:       0f b6 48 ff             movzbl 0xffffffffffffffff(%rax),%ecx
  400438:       48 83 c2 01             add    $0x1,%rdx
  40043c:       84 c9                   test   %cl,%cl
  40043e:       88 4a ff                mov    %cl,0xffffffffffffffff(%rdx)
  400441:       75 ed                   jne    400430 <main+0x40>
  ...

Ainsi, la première boucle fait 7 instructions tandis que la seconde en fait 6, même si elles sont censées faire le même travail. Maintenant, je ne peux pas vraiment dire s'il y a une certaine intelligence du compilateur derrière cela, probablement pas et c'est juste une coïncidence, mais je n'ai pas vérifié comment il interagit avec les autres options de compilateur que ce projet pourrait utiliser.


Sur clang 3.3 (-O3) en revanche, les deux boucles génèrent ce code à 5 instructions:

  400520:       8a 88 a0 06 40 00       mov    0x4006a0(%rax),%cl
  400526:       88 4c 04 10             mov    %cl,0x10(%rsp,%rax,1)
  40052a:       48 ff c0                inc    %rax
  40052d:       48 83 f8 05             cmp    $0x5,%rax
  400531:       75 ed                   jne    400520 <main+0x20>

Ce qui montre que les compilateurs sont assez différents et avancent à un rythme beaucoup plus rapide que certains programmeurs ne l'avaient prévu il y a plusieurs années. Cela signifie également que ce commentaire est assez vide de sens et probablement là parce que personne n'avait jamais vérifié si cela avait encore du sens.


Bottom line - si vous voulez optimiser le meilleur code possible (et vous savez à quoi il devrait ressembler), faites-le directement dans Assembly et coupez le "middle-man" (compilateur) de l'équation, mais prenez en compte ce nouveau les compilateurs et les nouveaux HW pourraient rendre cette optimisation obsolète. Dans la plupart des cas, il est préférable de laisser le compilateur faire ce niveau de travail pour vous et de vous concentrer sur l'optimisation des gros trucs.

Un autre point qui devrait être souligné - le nombre d'instructions (en supposant que c'est le code d'origine des PO d'origine), n'est en aucun cas une bonne mesure de l'efficacité du code. Toutes les instructions n'ont pas été créées égales, et certaines d'entre elles (simples mouvements de reg-to-reg par exemple) sont vraiment bon marché car elles sont optimisées par le CPU. Une autre optimisation pourrait en fait nuire aux optimisations internes du processeur, donc finalement, seuls les tests de performances appropriés comptent.

16
Leeor

Une boucle while est souvent compilée comme une boucle do-while Avec une branche initiale à la condition, c.-à-d.

    bra $1    ; unconditional branch to the condition
$2:
    ; loop body
$1:
    tst <condition> ; the condition
    brt $2    ; branch if condition true

tandis que la compilation d'une boucle do-while est la même sans la branche initiale. Vous pouvez en voir que while() est intrinsèquement moins efficace par le coût de la branche initiale, qui n'est cependant payée qu'une seule fois. [Comparez avec la façon naïve d'implémenter while, Qui nécessite à la fois une branche conditionnelle et une branche inconditionnelle par itération.]

Cela dit, ce ne sont pas des alternatives vraiment comparables. Il est pénible de transformer une boucle while en boucle do-while Et vice versa. Ils font des choses différentes. Et dans ce cas, les plusieurs appels de méthode domineraient totalement tout ce que le compilateur faisait avec while contre do-while.

10
user207421

La remarque ne concerne pas le choix de l'instruction de contrôle (do vs while), il s'agit du déroulement de la boucle !!!

Comme vous pouvez le voir, il s'agit d'une fonction de comparaison de chaînes (les éléments de chaîne faisant probablement 2 octets de long), qui auraient pu être écrits avec une seule comparaison plutôt que quatre dans un raccourci et une expression.

Cette dernière implémentation est à coup sûr plus rapide, car elle effectue une seule vérification de la condition de fin de chaîne après chaque comparaison de quatre éléments, tandis que le codage standard impliquerait une vérification par comparaison. Autrement dit, 5 tests pour 4 éléments contre 8 tests pour 4 éléments.

Quoi qu'il en soit, cela ne fonctionnera que si la longueur de chaîne est un multiple de quatre ou a un élément sentinelle (de sorte que les deux chaînes sont garanties différentes au-delà de la frontière strend). Assez risqué!

7
Yves Daoust

Cette discussion de l'efficacité while vs. do est complètement inutile dans ce cas, car il n'y a pas de corps.

while (Condition)
{
}

et

do
{
}
while (Condition);

sont absolument équivalents.

0
Yves Daoust