web-dev-qa-db-fra.com

Lequel est le plus rapide: tant que (1) ou tant que (2)?

C'était une question d'entretien posée par un haut responsable.

Lequel est plus vite? 

while(1) {
    // Some code
}

ou 

while(2) {
    //Some code
}

J'ai dit que les deux ont la même vitesse d'exécution, car l'expression dans while devrait finalement être évaluée à true ou false. Dans ce cas, les deux évaluent true et il n'y a pas d'instructions conditionnelles supplémentaires dans la condition while. Donc, les deux auront la même vitesse d'exécution et je préfère while (1).

Mais l'intervieweur dit avec assurance: "Vérifie tes bases. while(1) est plus rapide que while(2)." (Il ne testait pas ma confiance)

Est-ce vrai?

Voir aussi: Est-ce que "pour (;;)" est plus rapide que "while (TRUE)"? Sinon, pourquoi les gens l'utilisent-ils?

570
Nikole

Les deux boucles sont infinies, mais on peut voir laquelle prend plus d'instructions/ressources par itération.

À l’aide de gcc, j’ai compilé les deux programmes suivants pour Assembly à différents niveaux d’optimisation:

int main(void) {
    while(1) {}
    return 0;
}


int main(void) {
    while(2) {}
    return 0;
}

Même sans optimisation (-O0), l’Assemblée générée était identique pour les deux programmes . Par conséquent, il n’ya pas de différence de vitesse entre les deux boucles.

Pour référence, voici l’Assemblée générée (en utilisant gcc main.c -S -masm=intel avec un indicateur d’optimisation):

Avec -O0:

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    Push    rbp
    .seh_pushreg    rbp
    mov rbp, rsp
    .seh_setframe   rbp, 0
    sub rsp, 32
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Avec -O1:

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    sub rsp, 40
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Avec -O2 et -O3 (même résultat):

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .section    .text.startup,"x"
    .p2align 4,,15
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    sub rsp, 40
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

En fait, l'assemblage généré pour la boucle est identique pour tous les niveaux d'optimisation:

 .L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Les bits importants étant:

.L2:
    jmp .L2

Je ne comprends pas très bien l'Assemblée, mais il s'agit évidemment d'une boucle inconditionnelle. L'instruction jmp réinitialise inconditionnellement le programme sur l'étiquette .L2 sans même comparer une valeur à true, et le fait bien sûr immédiatement jusqu'à ce que le programme soit terminé. Ceci correspond directement au code C/C++:

L2:
    goto L2;

Modifier:

Il est intéressant de noter que même avec aucune optimisation, les boucles suivantes produisent toutes le même résultat exact (inconditionnel jmp) dans Assembly:

while(42) {}

while(1==1) {}

while(2==2) {}

while(4<7) {}

while(3==3 && 4==4) {}

while(8-9 < 0) {}

while(4.3 * 3e4 >= 2 << 6) {}

while(-0.1 + 02) {}

Et même à mon grand étonnement:

#include<math.h>

while(sqrt(7)) {}

while(hypot(3,4)) {}

Les choses deviennent un peu plus intéressantes avec les fonctions définies par l'utilisateur:

int x(void) {
    return 1;
}

while(x()) {}


#include<math.h>

double x(void) {
    return sqrt(7);
}

while(x()) {}

Au -O0, ces deux exemples appellent en fait x et effectuent une comparaison pour chaque itération.

Premier exemple (retourne 1):

.L4:
    call    x
    testl   %eax, %eax
    jne .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Deuxième exemple (retournant sqrt(7)):

.L4:
    call    x
    xorpd   %xmm1, %xmm1
    ucomisd %xmm1, %xmm0
    jp  .L4
    xorpd   %xmm1, %xmm1
    ucomisd %xmm1, %xmm0
    jne .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

Cependant, à partir de -O1 et ci-dessus, ils produisent tous deux le même assemblage que les exemples précédents (une jmp inconditionnelle dans l'étiquette précédente).

TL; DR

Sous GCC, les différentes boucles sont compilées pour un assemblage identique. Le compilateur évalue les valeurs constantes et n'effectue aucune comparaison réelle.

La morale de l'histoire est la suivante:

  • Il existe une couche de traduction entre le code source C++ et les instructions de la CPU, et cette couche a des implications importantes pour les performances.
  • Par conséquent, les performances ne peuvent pas être évaluées en ne regardant que le code source.
  • Le compilateur devrait être assez intelligent pour optimiser de tels cas triviaux. Les programmeurs ne devraient pas perdent leur temps à penser à eux dans la grande majorité des cas.
665

Oui, while(1) est beaucoup plus rapide que while(2), pour un humain à lire! Si je vois while(1) dans une base de code inconnue, je sais tout de suite ce que l'auteur avait l'intention de faire et mes yeux peuvent continuer à la ligne suivante.

Si je vois while(2), je vais probablement m'arrêter et essayer de comprendre pourquoi l'auteur n'a pas écrit while(1). Le doigt de l'auteur at-il glissé sur le clavier? Les responsables de cette base de code utilisent-ils while(n) en tant que mécanisme de commentaire obscur pour rendre les boucles différentes? Est-ce une solution de contournement grossière pour un avertissement fallacieux dans un outil d'analyse statique brisé? Ou est-ce un indice que je lis le code généré? S'agit-il d'un bug résultant d'une recherche ou d'un remplacement mal avisé, d'une mauvaise fusion ou d'un rayon cosmique? Peut-être que cette ligne de code est supposée faire quelque chose de radicalement différent. Peut-être était-il supposé lire while(w) ou while(x2). Je ferais mieux de trouver l'auteur dans l'historique du fichier et de lui envoyer un email "WTF" ... et maintenant j'ai brisé mon contexte mental. while(2) peut prendre plusieurs minutes de mon temps, quand while(1) aurait pris une fraction de seconde!

J'exagère, mais seulement un peu. La lisibilité du code est vraiment importante. Et cela mérite d'être mentionné dans une interview!

263
Chris Culter

Les réponses existantes montrant le code généré par un compilateur particulier pour une cible particulière avec un ensemble d’options particulier ne répondent pas complètement à la question - à moins que la question ait été posée dans ce contexte spécifique ("Ce qui est plus rapide avec gcc 4.7.2 pour x86_64 avec les options par défaut? ", par exemple).

En ce qui concerne la définition du langage, dans la machine abstraitewhile (1) évalue la constante entière 1 et while (2) évalue la constante entière 2; dans les deux cas, le résultat est comparé pour égalité à zéro. La norme de langage ne dit absolument rien sur la performance relative des deux constructions.

J'imagine qu'un compilateur extrêmement naïf pourrait générer un code machine différent pour les deux formulaires, du moins lorsqu'il est compilé sans demander d'optimisation.

D'autre part, les compilateurs C doivent absolument évaluer certaines expressions constantes au moment de la compilation, lorsqu'elles apparaissent dans des contextes nécessitant une expression constante. Par exemple, ceci:

int n = 4;
switch (n) {
    case 2+2: break;
    case 4:   break;
}

nécessite un diagnostic; un compilateur différé n'a pas l'option de différer l'évaluation de 2+2 jusqu'au moment de l'exécution. Comme un compilateur doit avoir la capacité d’évaluer des expressions constantes au moment de la compilation, il n’ya aucune bonne raison pour qu’il ne profite pas de cette possibilité, même si elle n’est pas requise.

La norme C ( N1570 6.8.5p4) indique que 

Une instruction d'itération entraîne la création d'une instruction appelée loop body exécuté à plusieurs reprises jusqu'à ce que l'expression de contrôle compare égale à 0.

Les expressions constantes pertinentes sont donc 1 == 0 et 2 == 0, les deux valeurs étant évaluées à la valeur int de 0. (Ces comparaisons sont implicites dans la sémantique de la boucle while; elles n'existent pas en tant qu'expressions C réelles.)

Un compilateur naïf et pervers pourrait générer un code différent pour les deux constructions. Par exemple, pour le premier, il pourrait générer une boucle infinie inconditionnelle (en considérant 1 comme un cas spécial) et pour le second, une comparaison explicite à l'exécution équivalente à 2 != 0. Mais je n'ai jamais rencontré de compilateur C qui se comporte de cette manière et je doute sérieusement qu'un tel compilateur existe.

La plupart des compilateurs (je suis tenté de dire que tous les compilateurs de qualité de production) ont des options pour demander des optimisations supplémentaires. Avec une telle option, il est encore moins probable qu'un compilateur génère un code différent pour les deux formulaires.

Si votre compilateur génère un code différent pour les deux constructions, commencez par vérifier si les séquences de code différentes ont des performances différentes. Si tel est le cas, essayez à nouveau de compiler avec une option d'optimisation (si disponible). S'ils diffèrent encore, envoyez un rapport de bogue au fournisseur du compilateur. Ce n'est pas (nécessairement) un bug dans le sens d'un échec pour se conformer à la norme C, mais c'est presque certainement un problème qui devrait être corrigé.

En bout de ligne: while (1) et while(2)presque ont certainement les mêmes performances. Ils ont exactement la même sémantique, et il n'y a aucune raison pour qu'un compilateur ne génère pas un code identique.

Et bien qu'il soit parfaitement légal pour un compilateur de générer du code plus rapide pour while(1) que pour while(2), il est tout aussi légal pour un compilateur de générer du code plus rapide pour while(1) que pour une autre occurrence de while(1) dans le même programme.

(Il y a une autre question implicite dans celle que vous avez posée: comment traitez-vous avec un intervieweur qui insiste sur un point technique incorrect? Ce serait probablement une bonne question pour le site Workplace ).

151
Keith Thompson

Attends une minute. L'intervieweur, est-ce qu'il ressemble à ce type?

 enter image description here

C’est déjà assez mauvais que l’intervieweur lui-même a échoué cette interview, Et si d’autres programmeurs de cette société avaient "réussi" ce test?

Non. Évaluation des instructions 1 == 0 et 2 == 0devrait être également rapidement. Nous imaginons} _ de mauvaises implémentations de compilateur où l’une pourrait être plus rapide que l’autre. Mais il n'y a pas de bonne raison pour laquelle l'un devrait être plus rapide que l'autre.

Même s'il existe des circonstances obscures dans lesquelles l'affirmation serait vraie, les programmeurs ne doivent pas être évalués sur la base de connaissances obscures (et dans ce cas effrayantes). Ne vous inquiétez pas pour cette interview, le mieux est de partir.

_ {Avertissement: Ceci n'est PAS un dessin original de Dilbert. Ceci est simplement un mashup .

134
janos

Votre explication est correcte. Cela semble être une question qui teste votre confiance en soi en plus des connaissances techniques.

Au fait, si vous avez répondu

Les deux morceaux de code sont également rapides, car les deux prennent un temps infini pour terminer

l'intervieweur dirait

Mais while (1) peut faire plus d'itérations par seconde; pouvez-vous expliquer pourquoi? (c'est absurde; tester votre confiance à nouveau)

Donc, en répondant comme vous l'avez fait, vous avez économisé un temps que vous auriez autrement perdu à discuter de cette mauvaise question.


Voici un exemple de code généré par le compilateur sur mon système (MS Visual Studio 2012), avec optimisations désactivées:

yyy:
    xor eax, eax
    cmp eax, 1     (or 2, depending on your code)
    je xxx
    jmp yyy
xxx:
    ...

Avec les optimisations activées:

xxx:
    jmp xxx

Le code généré est donc exactement le même, du moins avec un compilateur optimiseur.

75
anatolyg

L'explication la plus plausible à la question est que l'intervieweur pense que le processeur vérifie les bits individuels des nombres, un par un, jusqu'à ce qu'il atteigne une valeur non nulle:

1 = 00000001
2 = 00000010

Si le "est zéro?" L'algorithme commence à partir du côté droit du nombre et doit vérifier chaque bit jusqu'à atteindre un bit différent de zéro. La boucle while(1) { } devra vérifier deux fois plus de bits par itération que la boucle while(2) { }.

Cela nécessite un très mauvais modèle mental du fonctionnement des ordinateurs, mais il a sa propre logique interne. Une façon de vérifier serait de demander si while(-1) { } ou while(3) { } serait aussi rapide, ou si while(32) { } serait même plus lent .

58
Ryan Cavanaugh

Bien sûr, je ne connais pas les véritables intentions de ce manager, mais je propose un point de vue complètement différent: lors de l’embauche d’un nouveau membre dans une équipe, il est utile de savoir comment il réagit aux situations de conflit.

Ils vous ont conduit dans un conflit. Si cela est vrai, ils sont intelligents et la question était bonne. Pour certains secteurs, tels que le secteur bancaire, signaler votre problème à Stack Overflow peut constituer un motif de rejet.

Mais bien sûr, je ne sais pas, je ne propose qu'une option.

32
Tõnu Samuel

Je pense que l’indice se trouve dans "demandé par un cadre supérieur". Il est évident que cette personne a cessé de programmer quand il est devenu responsable, puis que cela lui a pris plusieurs années avant de devenir responsable. Jamais perdu intérêt pour la programmation, mais jamais écrit une ligne depuis ces jours. Ainsi, sa référence n'est pas "n'importe quel compilateur décent là-bas" comme certaines réponses le mentionnent, mais "le compilateur avec lequel cette personne a travaillé il y a 20-30 ans". 

À cette époque, les programmeurs passaient un pourcentage considérable de leur temps à essayer diverses méthodes pour rendre leur code plus rapide et plus efficace, car le temps de calcul du «mini-ordinateur central» était si précieux. De même que les gens écrivant des compilateurs. J'imagine que le compilateur unique mis à disposition par son entreprise à ce moment-là était optimisé sur la base des "déclarations fréquemment rencontrées pouvant être optimisées" et prenait un court raccourci lorsqu'il rencontrait un moment (1) et évaluait tout. sinon, y compris un moment (2). Avoir eu une telle expérience pourrait expliquer sa position et sa confiance en elle. 

La meilleure approche pour vous engager est probablement celle qui permet au cadre supérieur de s’emballer et de vous faire une conférence de 2-3 minutes sur «le bon vieux temps de la programmation» avant VOUS en douceur le guider vers le prochain sujet de l’entrevue. (Un bon timing est important ici - trop vite et vous interrompez l'histoire - trop lentement et vous êtes étiqueté comme quelqu'un avec une concentration insuffisante). Dites-lui à la fin de l'entretien que vous seriez très intéressé à en apprendre davantage sur ce sujet. 

25
OldFrank

Vous auriez dû lui demander comment il en était arrivé à cette conclusion. Sous n'importe quel compilateur décent, les deux compilent les mêmes instructions asm. Il aurait donc dû vous dire de commencer par le compilateur. Et même dans ce cas, vous devriez très bien connaître le compilateur et la plate-forme pour pouvoir même faire une supposition théorique. Et en fin de compte, cela n'a pas vraiment d'importance dans la pratique, car il existe d'autres facteurs externes, tels que la fragmentation de la mémoire ou la charge du système, qui vont influer davantage sur la boucle que ce détail.

19
Valentin Radu

Pour les besoins de cette question, je devrais ajouter ceci. Je me souviens de Doug Gwyn, du Comité C, écrivant que certains des premiers compilateurs C sans la passe d'optimisation généreraient un test dans Assembly pour le while(1) (comparé à for(;;) qui ne l'aurait pas).

Je répondrais à l'intervieweur en donnant cette note historique, puis je dirais que même si un compilateur le ferait très surpris, un compilateur aurait pu:

  • sans optimiseur, le compilateur génère un test pour while(1) et while(2)
  • avec optimizer pass, il est demandé au compilateur d’optimiser (avec un saut inconditionnel) tous les while(1) car ils sont considérés comme idiomatiques. Cela laisserait le while(2) avec un test et donc une différence de performance entre les deux.

J'ajouterais bien sûr à l'intervieweur que le fait de ne pas prendre en compte while(1) et while(2) est un signe d'optimisation de qualité médiocre, car ce sont des constructions équivalentes.

18
ouah

Une autre question sur une telle question serait de voir si vous avez le courage de dire à votre responsable qu’il a tort! Et comme vous pouvez le communiquer doucement. 

Mon premier instinct aurait été de générer une sortie Assembly pour montrer au gestionnaire que tout compilateur décent devrait s'en charger, et si ce n'est pas le cas, vous devrez soumettre le prochain correctif pour cela :)

9
UncleKing

Voir autant de personnes se plonger dans ce problème montre exactement pourquoi cela pourrait très bien être un test pour voir à quelle vitesse vous voulez micro-optimiser choses.

Ma réponse serait; peu importe, je me concentre plutôt sur le problème commercial que nous résolvons. Après tout, c'est pour ça que je vais être payé.

De plus, j'opterais pour while(1) {} parce que c'est plus courant et que les autres coéquipiers n'auraient pas besoin de passer du temps pour comprendre pourquoi quelqu'un choisirait un nombre supérieur à 1.

Maintenant, écris du code. ;-)

8
Bart Verkoeijen

Il me semble que c’est l’une de ces questions d’entrevue sur le comportement masquée comme une question technique. Certaines entreprises le font - elles poseront une question technique à laquelle tout programmeur compétent devrait pouvoir répondre assez facilement, mais lorsque l’interviewé donnera la bonne réponse, l’interviewer leur dira qu’elles ont tort.

L'entreprise veut savoir comment vous allez réagir dans cette situation. Est-ce que vous restez assis tranquillement et que vous ne répondez pas que votre réponse est correcte, en raison d'un doute de vous-même ou de la peur de déranger l'intervieweur? Ou êtes-vous prêt à défier une personne en position d'autorité qui se trompe? Ils veulent voir si vous êtes prêt à défendre vos convictions et si vous pouvez le faire avec tact et respect.

4
pacoverflow

Si vous êtes préoccupé par l'optimisation, vous devriez utiliser

for (;;)

parce que cela n'a pas de tests. (mode cynique)

4
Tom Tanner

J'avais l'habitude de programmer le code C et Assembly à l'époque où ce genre de non-sens aurait pu faire la différence. Quand cela a fait une différence, nous l'avons écrit en assemblée.

Si on m'avait posé cette question, j'aurais répété la célèbre citation de Donald Knuth de 1974 sur l'optimisation prématurée et j'ai marché si l'intervieweur ne riait pas et ne passait pas à autre chose.

3
Peter Wooster

L’intervieweur a peut-être posé intentionnellement une question aussi stupide et vous a demandé de faire trois remarques:

  1. Raisonnement de base. Les deux boucles sont infinies, il est difficile de parler de performance.
  2. Connaissance des niveaux d'optimisation. Il voulait avoir de vos nouvelles si vous laissiez le compilateur faire une optimisation pour vous, cela optimiserait la condition, surtout si le bloc n'était pas vide.
  3. Connaissance de l'architecture du microprocesseur. La plupart des architectures ont une instruction spéciale du processeur pour la comparaison avec 0 (sans nécessairement être plus rapide). 
3
Rok Kralj

Ils sont tous deux égaux - les mêmes.

Selon les spécifications, tout ce qui n'est pas 0 est considéré vrai, donc même sans optimisation, un bon compilateur ne générera aucun code. Le compilateur générerait une vérification simple pour != 0.

2
Nikolay Ivanchev

Voici un problème: si vous écrivez réellement un programme et mesurez sa vitesse, la vitesse des deux boucles pourrait être différente! Pour une comparaison raisonnable: 

unsigned long i = 0;
while (1) { if (++i == 1000000000) break; }

unsigned long i = 0;
while (2) { if (++i == 1000000000) break; }

avec un code ajouté qui affiche l'heure, un effet aléatoire tel que le positionnement de la boucle sur une ou deux lignes de cache peut faire la différence. Une boucle pourrait par hasard être complètement dans une ligne de cache, ou au début d'une ligne de cache, ou pourrait chevaucher deux lignes de cache. Et par conséquent, tout ce que l'intervieweur prétend être le plus rapide peut en réalité être le plus rapide - par coïncidence. 

Scénario le plus défavorable: un compilateur d'optimisation ne comprend pas ce que fait la boucle, mais détermine que les valeurs générées lors de l'exécution de la deuxième boucle sont identiques à celles produites par la première. Et générez le code complet pour la première boucle, mais pas pour la seconde. 

2
gnasher729

A en juger par le temps et les efforts que les gens ont consacrés à tester, prouver et répondre à cette question très simple, je dirais que les deux ont été ralentis très lentement en posant la question.

Et pour y passer encore plus de temps ...

"while (2)" est ridicule, car

"while (1)" et "while (true)" sont utilisés historiquement pour créer une boucle infinie dans laquelle on s'attend à ce que "break" soit appelé à un moment donné dans la boucle en fonction d'une condition qui se produira certainement. 

Le "1" est simplement là pour toujours évaluer comme vrai et par conséquent, dire "tant que (2)" est aussi bête que de dire "tant que (1 + 1 == 2)" qui sera également considéré comme vrai.

Et si vous voulez être complètement stupide, utilisez simplement: -

while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4)) {
    if (succeed())
        break;
}

Je voudrais bien penser que votre codeur a fait une faute de frappe qui n’a pas affecté le code, mais s’il a utilisé intentionnellement le "2" juste pour être bizarre, il sera viré avant qu’il ne mette bizarrement dans votre code, rendant difficile lire et travailler avec.

2
ekerner

Cela dépend du compilateur.

Si le code est optimisé ou s'il évalue 1 et 2 à vrai avec le même nombre d'instructions pour un jeu d'instructions particulier, la vitesse d'exécution sera la même.

Dans les cas réels, cela sera toujours aussi rapide, mais il serait possible d’imaginer un compilateur et un système particuliers lorsque cela serait évalué différemment.

Je veux dire: ce n’est pas vraiment une question liée à la langue (C).

1
user143522

La réponse évidente est la suivante: comme indiqué, les deux fragments exécuteraient une boucle infinie tout aussi occupée, ce qui rend le programme infiniment lent .

Bien que la redéfinition des mots clés C en tant que macros ait techniquement un comportement indéfini, c’est le seul moyen auquel je puisse penser pour créer rapidement un fragment de code: vous pouvez ajouter cette ligne au-dessus des 2 fragments:

#define while sleep

while(1) sera en effet deux fois plus rapide (ou moitié moins lent) que while(2)

0
chqrlie

Puisque les personnes qui cherchent à répondre à cette question veulent la boucle la plus rapide, j’aurais répondu que les deux compilaient également dans le même code Assembly, comme indiqué dans les autres réponses. Néanmoins, vous pouvez suggérer à l’intervieweur d’utiliser 'loop en déroulant'; une boucle {} boucle while au lieu de la boucle while. 

Prudent: Vous devez vous assurer que la boucle sera au moins toujours exécutée une fois .

La boucle devrait avoir une condition de rupture à l'intérieur.

De plus, pour ce type de boucle, je préférerais personnellement utiliser do {} while (42), car tout entier, à l'exception de 0, ferait l'affaire.

0
Antonin GAVREL