89 votes

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 de Chrome, parmi beaucoup d'autres) ce qui implique qu'une boucle while en C génère une "meilleure" 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

Est-il des preuves que la plupart (ou tous) les compilateurs donnerait de meilleurs (par exemple, plus efficace) code?

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

109voto

Mysticial Points 180300

Tout d'abord:

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

  • while et for boucles ne peuvent pas exécuter le corps de la boucle.
  • Un do-while boucle s'exécute toujours le corps de la boucle au moins une fois - il saute la première condition de vérifier.

Donc, c'est la logique de la différence. Cela dit, tout le monde ne se conforme strictement à cela. Il est assez commun pour while ou for boucles pour être utilisé même lorsqu'il est garanti que ce sera toujours de la boucle au moins une fois. (En particulier dans les langues avec foreach boucles.)

Donc pour éviter de comparer des pommes et des oranges, je vais en supposant que la boucle sera toujours exécuté au moins une fois. En outre, je ne vais pas mentionner for boucles de nouveau, car ils sont essentiellement while boucles avec un peu de syntaxe de sucre pour un compteur de boucle.

Donc je vais répondre à la question:

Si un while boucle est garanti à boucle au moins une fois, est-il un gain de performance de l'aide d'un do-while boucle à la place.


Un do-while saute la première condition de vérifier. Donc il y a moins de branche et un de moins condition à évaluer.

Si la condition est cher pour le vérifier, et vous savez que vous avez la garantie de la boucle au moins une fois, puis un do-while boucle pourrait être plus rapide.

Et tout cela est considéré comme un micro-optimisation, au mieux, il est un fait que le compilateur ne peut pas toujours faire: plus Précisément, lorsque le compilateur est pas en mesure de prouver que la boucle sera toujours saisir au moins une fois.


En d'autres termes, un tout-en boucle:

while (condition){
    body
}

Est effectivement la même chose que ceci:

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

Si vous savez que vous aurez toujours la boucle au moins une fois, que si l'instruction est superflu.


De même au niveau de l'assemblage, c'est à peu près la façon dont les différentes boucles de la compilation:

-lors de la boucle:

start:
    body
    test
    conditional jump to start

tout en boucle:

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

Notez que la condition a été dupliqué. Une autre approche est la suivante:

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

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

De toute façon, c'est encore pire que la normale do-while boucle.

Cela dit, les compilateurs peuvent faire ce qu'ils veulent. Et si elles peuvent prouver que la boucle est toujours entre une fois, puis il a fait le travail pour vous.


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

FWIW, je l'ai testé dans Visual Studio 2012:

  • Avec le vide d'un corps, il en fait de générer le même code pour while et do-while. De sorte qu'une partie est probablement un vestige de l'époque où les compilateurs n'étaient pas aussi grande.

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

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

Considérant la façon dont l'ancien commentaire, nous ne pouvons que deviner pourquoi il aurait de l'importance. Il est très possible que les compilateurs à l'époque n'étaient pas capables de reconnaître que le corps était vide. (Ou s'ils le faisaient, ils n'avaient pas l'utilisation de l'information.)

16voto

Leeor Points 6919

En un mot (tl;dr):

Je suis d'interpréter le commentaire à l'OPs code un peu différemment, je pense que le "meilleur code" ils prétendent avoir observé était dû au déplacement de la réelle travail dans la boucle "condition". Je suis complètement d'accord, cependant, que c'est très compilateur spécifique et que la comparaison ils ont fait, tout en étant capable de produire un code légèrement différent, est le plus souvent inutile et probablement obsolète, comme je l'ai montré ci-dessous.


Détails:

Il est difficile de dire ce que l'auteur veut dire par ses commentaires au sujet de cette do {} while la production de code meilleur, mais je tiens à spéculer dans un autre sens que ce qui a été soulevé ici, nous pensons que la différence entre do {} while et while {} boucles est assez mince (moins de la branche Mystique de le dire), mais il y a quelque chose de "drôle" dans le présent code et qui met tout le travail à l'intérieur de ce fou de la condition, et de garder la partie interne vide (do {}).

J'ai essayé le code suivant sur gcc 4.8.1 (-O3), et il 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>
  ...

Donc, la première boucle n'7 instructions tandis que le second n'6, même s'ils sont censés faire le même travail. Maintenant, je ne peux pas vraiment dire s'il existe un compilateur intelligence derrière tout cela, probablement pas, et c'est juste une coïncidence, mais je n'ai pas vérifié la façon dont il interagit avec d'autres options de compilateur de ce projet est peut-être en utilisant.


Sur clang 3.3 (-O3), d'autre part, les deux boucles de générer ce 5 instructions de code :

  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>

Qui va juste pour montrer que les compilateurs sont très différentes, et de faire progresser bien plus vite que certains programmeurs ont prévu il y a plusieurs années. Cela signifie également que ce commentaire est assez vide de sens et il y a probablement parce que personne n'avait jamais vérifié si il a encore un sens.


Bas de ligne - si vous souhaitez optimiser pour le meilleur code possible (et vous savez comment il devrait ressembler), de le faire directement dans l'assemblage et la découpe de la "middle-man" (le compilateur) à partir de l'équation, mais prendre en compte que les nouveaux compilateurs et les nouveaux HW pourrait faire cette optimisation obsolètes. Dans la plupart des cas, il est beaucoup mieux que de laisser le compilateur faire que le niveau de travail pour vous, et de se concentrer sur l'optimisation des gros trucs.

Un autre point qui devrait être fait - instruction count (en supposant que c'est ce que l'original OPs code a été après), n'est pas une bonne mesure pour l'efficacité du code. Pas toutes les instructions ont été créés égaux, et certains d'entre eux (simple reg-à-reg se déplace, par exemple) sont vraiment pas cher comme ils se sont optimisés par le CPU. D'autres optimisation pourrait blesser réellement interne de l'UC optimisations, donc finalement, seule l'analyse comparative des comptes de.

10voto

EJP Points 113412

Un while boucle est souvent compilés à l' do-while boucle avec une branche initiale de l'état, c'est à dire

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

alors que la compilation d'un do-while boucle est la même sans la branche initiale. Vous pouvez voir à partir de ce qu'il est intrinsèquement moins efficace par le coût de la première branche, qui est, cependant, seulement payé une fois. [Comparer à de la naïveté à la mise en oeuvre while, qui exige à la fois une branche conditionnelle et inconditionnelle de la branche par itération.]

Cela dit, ils ne sont pas vraiment comparable à celle des solutions de rechange. Il est douloureux pour transformer un while boucle en do-while boucle et vice versa. Ils font des choses différentes. Et dans ce cas, le plusieurs appels de méthode totalement dominer quel que soit le compilateur fait avec while contre do-while.

7voto

Yves Daoust Points 6396

La remarque n'est pas sur le choix du contrôle de la déclaration (vs), il est sur le déroulement de la boucle !!!

Comme vous pouvez le voir, c'est une fonction de comparaison de chaînes de caractères (string éléments étant probablement de 2 octets), ce qui pourrait avoir été écrit avec une seule comparaison plutôt que quatre dans un raccourci-et d'expression.

Cette dernière application est pour vous plus vite, comme il le fait d'une seule case à la fin de la chaîne état après tous les quatre élément comparaisons, alors que la norme de codage impliquerait un seul chèque par comparaison. Autrement dit, 5 tests par 4 élément vs 8 tests par 4 éléments.

De toute façon, il ne fonctionne que si la longueur de la chaîne est un multiple de quatre ou a une sentinelle de l'élément (de sorte que les deux chaînes sont garantis diffèrent passé l' strend de la frontière). Assez risqué !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X