web-dev-qa-db-fra.com

Si déclaration vs déclaration if-else, qui est plus rapide?

L'autre jour, je me suis disputé avec un ami au sujet de ces deux extraits. Lequel est plus rapide et pourquoi?

value = 5;
if (condition) {
    value = 6;
}

et:

if (condition) {
    value = 6;
} else {
    value = 5;
}

Et si value est une matrice?

Note: Je sais que value = condition ? 6 : 5; existe et je m'attends à ce qu'il soit plus rapide, mais ce n'était pas une option.

Edit (demandé par le personnel, la question étant en attente pour le moment):

  • veuillez répondre en considérant soit x86 Assembly généré par les compilateurs classiques (say g ++, clang ++, vc, mingw) dans les versions optimisées et non optimisées, soit MIPS Assembly.
  • quand Assembly diffère, expliquer pourquoi une version est plus rapide et quand (par exemple: "mieux car aucune branche n'a encore de problème blahblah")
79
Julien__

TL; DR: Dans le code non optimisé, if sans else semble plus efficace, mais même avec le niveau d'optimisation le plus élémentaire activé, le code est fondamentalement réécrit en value = condition + 5.


Je j'ai essayé et généré l'Assembly pour le code suivant:

int ifonly(bool condition, int value)
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return value;
}

int ifelse(bool condition, int value)
{
    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return value;
}

Sur gcc 6.3 avec les optimisations désactivées (-O0), la différence pertinente est la suivante:

 mov     DWORD PTR [rbp-8], 5
 cmp     BYTE PTR [rbp-4], 0
 je      .L2
 mov     DWORD PTR [rbp-8], 6
.L2:
 mov     eax, DWORD PTR [rbp-8]

pour ifonly, alors que ifelse

 cmp     BYTE PTR [rbp-4], 0
 je      .L5
 mov     DWORD PTR [rbp-8], 6
 jmp     .L6
.L5:
 mov     DWORD PTR [rbp-8], 5
.L6:
 mov     eax, DWORD PTR [rbp-8]

Ce dernier semble légèrement moins efficace car il comporte un saut supplémentaire, mais les deux ont au moins deux et au plus trois assignations. À moins que vous ne deviez vraiment tirer le maximum de votre performance (conseil: si vous travaillez sur une navette spatiale, vous ne le ferez pas.) et même alors vous probablement ne le faites pas), la différence ne sera pas perceptible.

Cependant, même avec le niveau d'optimisation le plus bas (-O1), les deux fonctions sont réduites au même:

test    dil, dil
setne   al
movzx   eax, al
add     eax, 5

qui est fondamentalement l'équivalent de 

return 5 + condition;

en supposant que condition est égal à zéro ou à un . Des niveaux d'optimisation supérieurs ne modifient pas vraiment la sortie, sauf qu'ils parviennent à éviter la movzx en mettant à zéro le registre EAX au début.


Disclaimer: Vous ne devriez probablement pas écrire 5 + condition vous-même (même si la norme garantit que la conversion de true en un type entier donne 1) car votre intention pourrait ne pas être immédiatement évidente pour les personnes qui lisent votre code (ce qui peut inclure votre code). futur moi). Le but de ce code est de montrer que ce que le compilateur produit dans les deux cas est (pratiquement) identique. Ciprian Tomoiaga le dit assez bien dans les commentaires:

le travail d'un humain consiste à écrire le code pour les humains et à laisser le compilateur écrire le code pour la machine

272
CompuChip

La réponse de CompuChip montre que, pour int, ils sont tous deux optimisés pour le même assemblage, donc peu importe.

Et si la valeur est une matrice?

Je vais interpréter cela d’une manière plus générale, c’est-à-dire si value est d’un type dont les constructions et les affectations sont coûteuses (et les déplacements sont bon marché).

puis

T value = init1;
if (condition)
   value = init2;

est sous-optimal car dans le cas où condition est vrai, vous effectuez l'initialisation inutile sur init1 et vous effectuez ensuite l'affectation de copie.

T value;
if (condition)
   value = init2;
else
   value = init3;

C'est mieux. Mais toujours sous-optimal si la construction par défaut est chère et si la construction de copie est plus chère alors l’initialisation.

Vous avez la solution opérateur conditionnelle qui est bonne:

T value = condition ? init1 : init2;

Ou, si vous n'aimez pas l'opérateur conditionnel, vous pouvez créer une fonction d'assistance comme celle-ci:

T create(bool condition)
{
  if (condition)
     return {init1};
  else
     return {init2};
}

T value = create(condition);

Selon ce que sont init1 et init2, vous pouvez également considérer ceci:

auto final_init = condition ? init1 : init2;
T value = final_init;

Mais, encore une fois, je dois souligner que cela n’est pertinent que lorsque la construction et les travaux sont vraiment coûteux pour un type donné. Et même alors, seulement en profilant vous êtes sûr.

44
bolov

Dans le code non optimisé, le premier exemple affecte une variable toujours une fois et parfois deux fois. Le deuxième exemple n'affecte une variable qu'une seule fois. Le conditionnel est le même sur les deux chemins de code, cela ne devrait donc pas avoir d'importance. En code optimisé, cela dépend du compilateur.

Comme toujours, si cela vous intéresse, générez l’Assembly et voyez ce que fait le compilateur.

10
Neil

Qu'est-ce qui vous ferait penser que l'un d'entre eux, même si la doublure est plus rapide ou plus lente? 

unsigned int fun0 ( unsigned int condition, unsigned int value )
{
    value = 5;
    if (condition) {
        value = 6;
    }
    return(value);
}
unsigned int fun1 ( unsigned int condition, unsigned int value )
{

    if (condition) {
        value = 6;
    } else {
        value = 5;
    }
    return(value);
}
unsigned int fun2 ( unsigned int condition, unsigned int value )
{
    value = condition ? 6 : 5;
    return(value);
}

Plus de lignes de code d'un langage de haut niveau donnent au compilateur plus de travail, donc si vous voulez en faire une règle générale, donnez au compilateur plus de code à utiliser. Si l'algorithme est identique aux cas ci-dessus, on peut s'attendre à ce que le compilateur avec une optimisation minimale comprenne cela.

00000000 <fun0>:
   0:   e3500000    cmp r0, #0
   4:   03a00005    moveq   r0, #5
   8:   13a00006    movne   r0, #6
   c:   e12fff1e    bx  lr

00000010 <fun1>:
  10:   e3500000    cmp r0, #0
  14:   13a00006    movne   r0, #6
  18:   03a00005    moveq   r0, #5
  1c:   e12fff1e    bx  lr

00000020 <fun2>:
  20:   e3500000    cmp r0, #0
  24:   13a00006    movne   r0, #6
  28:   03a00005    moveq   r0, #5
  2c:   e12fff1e    bx  lr

ce n’est pas une grande surprise, la première fonction a été exécutée dans un ordre différent, même temps d’exécution.

0000000000000000 <fun0>:
   0:   7100001f    cmp w0, #0x0
   4:   1a9f07e0    cset    w0, ne
   8:   11001400    add w0, w0, #0x5
   c:   d65f03c0    ret

0000000000000010 <fun1>:
  10:   7100001f    cmp w0, #0x0
  14:   1a9f07e0    cset    w0, ne
  18:   11001400    add w0, w0, #0x5
  1c:   d65f03c0    ret

0000000000000020 <fun2>:
  20:   7100001f    cmp w0, #0x0
  24:   1a9f07e0    cset    w0, ne
  28:   11001400    add w0, w0, #0x5
  2c:   d65f03c0    ret

Espérons que vous avez l’idée que vous auriez pu simplement essayer cela s’il n’était pas évident que les différentes implémentations ne soient pas réellement différentes.

En ce qui concerne une matrice, je ne sais pas comment cela compte,

if(condition)
{
 big blob of code a
}
else
{
 big blob of code b
}

je vais juste mettre le même wrapper if-then-else autour des gros blobs de code, qu’ils aient la valeur = 5 ou quelque chose de plus compliqué. De même, la comparaison, même s'il s'agit d'un gros bloc de code, doit toujours être calculée et égale ou non égale à quelque chose est souvent compilée avec le négatif, si (condition) faire quelque chose est souvent compilé comme si non condition goto.

00000000 <fun0>:
   0:   0f 93           tst r15     
   2:   03 24           jz  $+8         ;abs 0xa
   4:   3f 40 06 00     mov #6, r15 ;#0x0006
   8:   30 41           ret         
   a:   3f 40 05 00     mov #5, r15 ;#0x0005
   e:   30 41           ret         

00000010 <fun1>:
  10:   0f 93           tst r15     
  12:   03 20           jnz $+8         ;abs 0x1a
  14:   3f 40 05 00     mov #5, r15 ;#0x0005
  18:   30 41           ret         
  1a:   3f 40 06 00     mov #6, r15 ;#0x0006
  1e:   30 41           ret         

00000020 <fun2>:
  20:   0f 93           tst r15     
  22:   03 20           jnz $+8         ;abs 0x2a
  24:   3f 40 05 00     mov #5, r15 ;#0x0005
  28:   30 41           ret         
  2a:   3f 40 06 00     mov #6, r15 ;#0x0006
  2e:   30 41

nous venons de faire cet exercice avec quelqu'un d'autre récemment sur stackoverflow. Il est intéressant de noter que ce compilateur mips a non seulement réalisé que les fonctions étaient les mêmes, mais qu’il y avait une fonction qui sautait simplement à l’autre pour économiser de l’espace sur le code. Je n'ai pas fait ça ici

00000000 <fun0>:
   0:   0004102b    sltu    $2,$0,$4
   4:   03e00008    jr  $31
   8:   24420005    addiu   $2,$2,5

0000000c <fun1>:
   c:   0004102b    sltu    $2,$0,$4
  10:   03e00008    jr  $31
  14:   24420005    addiu   $2,$2,5

00000018 <fun2>:
  18:   0004102b    sltu    $2,$0,$4
  1c:   03e00008    jr  $31
  20:   24420005    addiu   $2,$2,5

quelques autres cibles.

00000000 <_fun0>:
   0:   1166            mov r5, -(sp)
   2:   1185            mov sp, r5
   4:   0bf5 0004       tst 4(r5)
   8:   0304            beq 12 <_fun0+0x12>
   a:   15c0 0006       mov $6, r0
   e:   1585            mov (sp)+, r5
  10:   0087            rts pc
  12:   15c0 0005       mov $5, r0
  16:   1585            mov (sp)+, r5
  18:   0087            rts pc

0000001a <_fun1>:
  1a:   1166            mov r5, -(sp)
  1c:   1185            mov sp, r5
  1e:   0bf5 0004       tst 4(r5)
  22:   0204            bne 2c <_fun1+0x12>
  24:   15c0 0005       mov $5, r0
  28:   1585            mov (sp)+, r5
  2a:   0087            rts pc
  2c:   15c0 0006       mov $6, r0
  30:   1585            mov (sp)+, r5
  32:   0087            rts pc

00000034 <_fun2>:
  34:   1166            mov r5, -(sp)
  36:   1185            mov sp, r5
  38:   0bf5 0004       tst 4(r5)
  3c:   0204            bne 46 <_fun2+0x12>
  3e:   15c0 0005       mov $5, r0
  42:   1585            mov (sp)+, r5
  44:   0087            rts pc
  46:   15c0 0006       mov $6, r0
  4a:   1585            mov (sp)+, r5
  4c:   0087            rts pc

00000000 <fun0>:
   0:   00a03533            snez    x10,x10
   4:   0515                    addi    x10,x10,5
   6:   8082                    ret

00000008 <fun1>:
   8:   00a03533            snez    x10,x10
   c:   0515                    addi    x10,x10,5
   e:   8082                    ret

00000010 <fun2>:
  10:   00a03533            snez    x10,x10
  14:   0515                    addi    x10,x10,5
  16:   8082                    ret

et compilateurs

avec ce code i on pourrait s'attendre à ce que les différentes cibles correspondent aussi bien

define i32 @fun0(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %. = select i1 %1, i32 6, i32 5
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun1(i32 %condition, i32 %value) #0 {
  %1 = icmp eq i32 %condition, 0
  %. = select i1 %1, i32 5, i32 6
  ret i32 %.
}

; Function Attrs: norecurse nounwind readnone
define i32 @fun2(i32 %condition, i32 %value) #0 {
  %1 = icmp ne i32 %condition, 0
  %2 = select i1 %1, i32 6, i32 5
  ret i32 %2
}


00000000 <fun0>:
   0:   e3a01005    mov r1, #5
   4:   e3500000    cmp r0, #0
   8:   13a01006    movne   r1, #6
   c:   e1a00001    mov r0, r1
  10:   e12fff1e    bx  lr

00000014 <fun1>:
  14:   e3a01006    mov r1, #6
  18:   e3500000    cmp r0, #0
  1c:   03a01005    moveq   r1, #5
  20:   e1a00001    mov r0, r1
  24:   e12fff1e    bx  lr

00000028 <fun2>:
  28:   e3a01005    mov r1, #5
  2c:   e3500000    cmp r0, #0
  30:   13a01006    movne   r1, #6
  34:   e1a00001    mov r0, r1
  38:   e12fff1e    bx  lr


fun0:
    Push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB0_2
    mov.w   #5, r15
.LBB0_2:
    pop.w   r4
    ret

fun1:
    Push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #5, r15
    cmp.w   #0, r12
    jeq .LBB1_2
    mov.w   #6, r15
.LBB1_2:
    pop.w   r4
    ret


fun2:
    Push.w  r4
    mov.w   r1, r4
    mov.w   r15, r12
    mov.w   #6, r15
    cmp.w   #0, r12
    jne .LBB2_2
    mov.w   #5, r15
.LBB2_2:
    pop.w   r4
    ret

Maintenant, techniquement, il existe une différence de performances entre certaines de ces solutions. Parfois, le résultat est 5 cas où le saut est 6, et inversement, une branche est-elle plus rapide que l'exécution? on pourrait discuter mais l'exécution devrait varier. Mais il s’agit plus d’une condition if si vs d’une condition non dans le code, ce qui a pour effet que le compilateur exécute la commande if si ce saut saute d’autre à s’exécuter. mais cela n'est pas nécessairement dû au style de codage mais à la comparaison et aux cas if et else dans une syntaxe quelconque.

8
old_timer

Ok, puisque Assembly est l’une des balises, je supposerai simplement que votre code est un pseudo-code (et pas nécessairement c) et le traduirons par un humain en 6502 Assembly.

1ère option (sans autre)

        ldy #$00
        lda #$05
        dey
        bmi false
        lda #$06
false   brk

2ème option (avec autre)

        ldy #$00
        dey
        bmi else
        lda #$06
        sec
        bcs end
else    lda #$05
end     brk

Hypothèses: la condition est dans le registre Y, définissez cette valeur sur 0 ou 1 sur la première ligne de l'une ou l'autre option, le résultat sera mis en mémoire.

Ainsi, après avoir compté les cycles pour les deux possibilités de chaque cas, nous voyons que la 1ère construction est généralement plus rapide; 9 cycles lorsque la condition est 0 et 10 cycles lorsque la condition est 1, alors que l'option deux est également 9 cycles lorsque la condition est 0, mais 13 cycles lorsque la condition est 1. (Les comptages de cycles n'incluent pas le BRK à la fin) .

Conclusion: If only est plus rapide que la construction If-Else.

Et pour être complet, voici une solution value = condition + 5 optimisée:

ldy #$00
lda #$00
tya
adc #$05
brk

Cela réduit notre temps à 8 cycles (encore en excluant le BRK à la fin).

0
Glen Yates