Imaginez le code assembleur qui serait généré à partir de :
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
Je suppose que ça 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 disposées dans un ordre tel que le bar
la casse précède le foo
cas (par opposition au code C). Cela peut mieux utiliser le pipeline CPU, car un saut écrase les instructions déjà récupérées.
Avant que le saut ne soit exécuté, les instructions en dessous (le bar
cas) sont poussés vers le pipeline. Depuis le foo
cas est peu probable, sauter aussi est peu probable, donc écraser le pipeline est peu probable.
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 !
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 le puts
puis retq
retour.
Avec __builtin_expect
Remplacez maintenant if (i)
avec :
if (__builtin_expect(i, 0))
et on obtient :
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
reviens !
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 s'exécute plus rapidement avec __builtin_expect
que sans, les processeurs sont vraiment intelligents ces jours-ci. Mes tentatives naïves sont là.
C++20 [[likely]]
et [[unlikely]]
C++20 a standardisé ces éléments intégrés C++ :Comment utiliser l'attribut probable/improbable de C++20 dans l'instruction if-else Ils feront probablement (un jeu de mots !) La même chose.
L'idée de __builtin_expect
est de dire au compilateur que vous trouverez généralement que l'expression est évaluée à c, afin que le compilateur puisse optimiser pour ce cas.
Je suppose que quelqu'un pensait qu'il était intelligent et qu'il accélérait 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 aurait bien pu empirer les choses. La documentation dit même :
En général, vous devriez préférer utiliser les commentaires de profil réels pour cela (
-fprofile-arcs
), car les programmeurs sont notoirement mauvais pour prédire les performances réelles 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 :
- Vous rencontrez un réel problème de performances
- Vous avez déjà correctement optimisé les algorithmes du système
- Vous disposez de données de performances pour étayer votre affirmation selon laquelle un cas particulier est le plus probable