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):
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
a
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.
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.
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.
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.
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).