Je suis tombé sur un #define
Dans lequel ils utilisent __builtin_expect
.
La documentation dit:
Fonction intégrée:
long __builtin_expect (long exp, long c)
Vous pouvez utiliser
__builtin_expect
Pour fournir au compilateur des informations de prédiction de branche. En général, vous devriez préférer utiliser les informations de retour de profil réelles pour cela (-fprofile-arcs
), Car les programmeurs sont notoirement mal à même de prédire les performances réelles de leurs programmes. Cependant, il existe des applications dans lesquelles ces données sont difficiles à collecter.La valeur de retour est la valeur de
exp
, qui devrait être une expression intégrale. La sémantique de la fonction intégrée est qu'il est prévu queexp == c
. Par exemple:if (__builtin_expect (x, 0)) foo ();
indiquerait que nous ne prévoyons pas d'appeler
foo
, puisque nous prévoyons quex
sera nul.
Alors pourquoi ne pas utiliser directement:
if (x)
foo ();
au lieu de la syntaxe compliquée avec __builtin_expect
?
Imaginez le code d'assemblage qui serait généré à partir de:
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
Je suppose que cela devrait être quelque chose comme:
cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
Vous pouvez voir que les instructions sont organisées dans un ordre tel que le cas bar
précède le cas foo
(par opposition au code C). Cela permet de mieux utiliser le pipeline du processeur, puisqu'un saut saute les instructions déjà extraites.
Avant que le saut ne soit exécuté, les instructions en dessous (la casse bar
sont transférées dans le pipeline). Puisque le cas foo
est improbable, il est également improbable de sauter, ce qui rend improbable le pipeline.
L'idée de __builtin_expect
consiste à indiquer au compilateur que vous constaterez généralement que l'expression est évaluée à c, afin que le compilateur puisse optimiser ce cas.
J'imagine que quelqu'un pensait qu'ils étaient intelligents et qu'ils accéléraient les choses en faisant cela.
Malheureusement, à moins que la situation ne soit très bien comprise (il est probable qu’ils n’aient rien fait de tel), cela pourrait bien avoir aggravé la situation. La documentation dit même:
En général, vous devriez préférer utiliser les commentaires du profil réel pour cela (
-fprofile-arcs
), les programmeurs sont notoirement mal à même de prédire le fonctionnement réel de leurs programmes. Cependant, il existe des applications dans lesquelles ces données sont difficiles à collecter.
En général, vous ne devriez pas utiliser __builtin_expect
sauf si:
Décompilons pour voir ce que GCC 4.8 en fait
Blagovest a mentionné l'inversion de branche pour améliorer le pipeline, mais les compilateurs actuels le font-ils vraiment? Découvrons-le!
Sans __builtin_expect
#include "stdio.h"
#include "time.h"
int main() {
/* Use time to prevent it from being optimized away. */
int i = !time(NULL);
if (i)
puts("a");
return 0;
}
Compiler et décompiler avec GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Sortie:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 75 0a jne 1a <main+0x1a>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1
15: e8 00 00 00 00 callq 1a <main+0x1a>
16: R_X86_64_PC32 puts-0x4
1a: 31 c0 xor %eax,%eax
1c: 48 83 c4 08 add $0x8,%rsp
20: c3 retq
L'ordre des instructions en mémoire était inchangé: d'abord puts
et ensuite retq
.
Avec __builtin_expect
Maintenant, remplacez if (i)
par:
if (__builtin_expect(i, 0))
et nous obtenons:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: 31 ff xor %edi,%edi
6: e8 00 00 00 00 callq b <main+0xb>
7: R_X86_64_PC32 time-0x4
b: 48 85 c0 test %rax,%rax
e: 74 07 je 17 <main+0x17>
10: 31 c0 xor %eax,%eax
12: 48 83 c4 08 add $0x8,%rsp
16: c3 retq
17: bf 00 00 00 00 mov $0x0,%edi
18: R_X86_64_32 .rodata.str1.1
1c: e8 00 00 00 00 callq 21 <main+0x21>
1d: R_X86_64_PC32 puts-0x4
21: eb ed jmp 10 <main+0x10>
Le puts
a été déplacé à la toute fin de la fonction, le retq
revient!
Le nouveau code est fondamentalement le même que:
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
Cette optimisation n'a pas été faite avec -O0
.
Mais bonne chance pour écrire un exemple qui tourne plus vite avec __builtin_expect
Que sans, les CPU sont vraiment intelligents ces jours-ci . Mes tentatives naïves sont ici .
Comme il est dit dans la description, la première version ajoute un élément prédictif à la construction, indiquant au compilateur que le x == 0
_ est la branche la plus probable - c’est-à-dire que votre programme s’occupera plus souvent.
En gardant cela à l'esprit, le compilateur peut optimiser la condition afin qu'elle nécessite le moins de travail possible lorsque la condition attendue se vérifie, au risque de devoir faire plus de travail en cas de condition imprévue.
Examinez comment les conditions sont implémentées pendant la phase de compilation, ainsi que dans l'assembly résultant, pour voir comment une branche peut nécessiter moins de travail que l'autre.
Cependant, je ne m'attendrais à ce que cette optimisation ait un effet notable si le conditionnel en question fait partie d'une boucle interne étroite appelée "lot", car la différence dans le code résultant est relativement petite. Et si vous l’optimisez dans le mauvais sens, vous risquez de diminuer vos performances.
Je ne vois aucune des réponses à la question que je pense que vous posiez, paraphrasez:
Existe-t-il un moyen plus portable d'indiquer la prédiction de branche au compilateur?.
Le titre de votre question m'a fait penser à le faire de cette façon:
if ( !x ) {} else foo();
Si le compilateur suppose que 'true' est plus probable, il pourrait optimiser pour ne pas appeler foo()
.
Le problème ici est simplement que, généralement, vous ne savez pas ce que le compilateur va supposer. Tout code utilisant ce type de technique doit donc être soigneusement mesuré (et éventuellement surveillé dans le temps si le contexte change).
Je le teste sur Mac selon @ Blagovest Buyukliev et @Ciro. Les assemblages semblent clairs et j'ajoute des commentaires;
Les commandes sont gcc -c -O3 -std=gnu11 testOpt.c; otool -tVI testOpt.o
Lorsque j'utilise -O3 ,, l'apparence est la même, peu importe le __builtin_expect (i, 0) existant ou non.
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp // open function stack
0000000000000004 xorl %edi, %edi // set time args 0 (NULL)
0000000000000006 callq _time // call time(NULL)
000000000000000b testq %rax, %rax // check time(NULL) result
000000000000000e je 0x14 // jump 0x14 if testq result = 0, namely jump to puts
0000000000000010 xorl %eax, %eax // return 0 , return appear first
0000000000000012 popq %rbp // return 0
0000000000000013 retq // return 0
0000000000000014 leaq 0x9(%rip), %rdi ## literal pool for: "a" // puts part, afterwards
000000000000001b callq _puts
0000000000000020 xorl %eax, %eax
0000000000000022 popq %rbp
0000000000000023 retq
Lors de la compilation avec -O2, l’apparence est différente avec et sans __builtin_expect (i, 0)
D'abord sans
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 xorl %edi, %edi
0000000000000006 callq _time
000000000000000b testq %rax, %rax
000000000000000e jne 0x1c // jump to 0x1c if not zero, then return
0000000000000010 leaq 0x9(%rip), %rdi ## literal pool for: "a" // put part appear first , following jne 0x1c
0000000000000017 callq _puts
000000000000001c xorl %eax, %eax // return part appear afterwards
000000000000001e popq %rbp
000000000000001f retq
Maintenant avec __builtin_expect (i, 0)
testOpt.o:
(__TEXT,__text) section
_main:
0000000000000000 pushq %rbp
0000000000000001 movq %rsp, %rbp
0000000000000004 xorl %edi, %edi
0000000000000006 callq _time
000000000000000b testq %rax, %rax
000000000000000e je 0x14 // jump to 0x14 if zero then put. otherwise return
0000000000000010 xorl %eax, %eax // return appear first
0000000000000012 popq %rbp
0000000000000013 retq
0000000000000014 leaq 0x7(%rip), %rdi ## literal pool for: "a"
000000000000001b callq _puts
0000000000000020 jmp 0x10
Pour résumer, __builtin_expect fonctionne dans le dernier cas.