Sie weisen den Compiler darauf hin, Anweisungen auszugeben, die bewirken, dass die Verzweigungsvorhersage die "wahrscheinliche" Seite einer Sprunganweisung bevorzugt. Dies kann ein großer Gewinn sein, wenn die Vorhersage korrekt ist, bedeutet dies, dass der Sprungbefehl im Grunde kostenlos ist und null Zyklen dauert. Wenn die Vorhersage andererseits falsch ist, bedeutet dies, dass die Prozessorpipeline geleert werden muss, was mehrere Zyklen kosten kann. Solange die Vorhersage die meiste Zeit richtig ist, ist dies tendenziell gut für die Leistung.
Wie bei allen solchen Leistungsoptimierungen sollten Sie dies nur nach einer umfassenden Profilerstellung tun, um sicherzustellen, dass sich der Code wirklich in einem Engpass befindet, und wahrscheinlich aufgrund der Mikronatur, dass er in einer engen Schleife ausgeführt wird. Im Allgemeinen sind die Linux-Entwickler ziemlich erfahren, also würde ich mir vorstellen, dass sie das getan hätten. Sie kümmern sich nicht wirklich um Portabilität, da sie nur auf gcc abzielen, und sie haben eine sehr genaue Vorstellung von der Assembly, die sie generieren möchten.
Lassen Sie uns dekompilieren, um zu sehen, was GCC 4.8 damit macht
Ohne __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)
printf("%d\n", i);
puts("a");
return 0;
}
Kompilieren und dekompilieren Sie mit GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Ausgabe:
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 14 jne 24 <main+0x24>
10: ba 01 00 00 00 mov $0x1,%edx
15: be 00 00 00 00 mov $0x0,%esi
16: R_X86_64_32 .rodata.str1.1
1a: bf 01 00 00 00 mov $0x1,%edi
1f: e8 00 00 00 00 callq 24 <main+0x24>
20: R_X86_64_PC32 __printf_chk-0x4
24: bf 00 00 00 00 mov $0x0,%edi
25: R_X86_64_32 .rodata.str1.1+0x4
29: e8 00 00 00 00 callq 2e <main+0x2e>
2a: R_X86_64_PC32 puts-0x4
2e: 31 c0 xor %eax,%eax
30: 48 83 c4 08 add $0x8,%rsp
34: c3 retq
Die Befehlsreihenfolge im Speicher blieb unverändert:zuerst die printf
und dann puts
und die retq
zurück.
Mit __builtin_expect
Ersetzen Sie nun if (i)
mit:
if (__builtin_expect(i, 0))
und wir erhalten:
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 11 je 21 <main+0x21>
10: bf 00 00 00 00 mov $0x0,%edi
11: R_X86_64_32 .rodata.str1.1+0x4
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
21: ba 01 00 00 00 mov $0x1,%edx
26: be 00 00 00 00 mov $0x0,%esi
27: R_X86_64_32 .rodata.str1.1
2b: bf 01 00 00 00 mov $0x1,%edi
30: e8 00 00 00 00 callq 35 <main+0x35>
31: R_X86_64_PC32 __printf_chk-0x4
35: eb d9 jmp 10 <main+0x10>
Die printf
(kompiliert zu __printf_chk
) wurde an das Ende der Funktion nach puts
verschoben und die Rückkehr zur Verbesserung der Verzweigungsvorhersage, wie in anderen Antworten erwähnt.
Es ist also im Grunde dasselbe wie:
int main() {
int i = !time(NULL);
if (i)
goto printf;
puts:
puts("a");
return 0;
printf:
printf("%d\n", i);
goto puts;
}
Diese Optimierung wurde nicht mit -O0
durchgeführt .
Aber viel Glück beim Schreiben eines Beispiels, das mit __builtin_expect
schneller läuft als ohne, CPUs sind heutzutage wirklich schlau. Meine naiven Versuche sind hier.
C++20 [[likely]]
und [[unlikely]]
C++20 hat diese eingebauten C++-Elemente standardisiert:How to use C++20's wahrscheinlich/unwahrscheinlich Attribut in if-else Statement Sie werden wahrscheinlich (ein Wortspiel!) dasselbe tun.
Dies sind Makros, die dem Compiler Hinweise geben, in welche Richtung eine Verzweigung gehen kann. Die Makros werden auf GCC-spezifische Erweiterungen erweitert, sofern verfügbar.
GCC verwendet diese, um die Verzweigungsvorhersage zu optimieren. Zum Beispiel, wenn Sie etwas wie das Folgende haben
if (unlikely(x)) {
dosomething();
}
return x;
Dann kann es diesen Code so umstrukturieren, dass er eher wie folgt aussieht:
if (!x) {
return x;
}
dosomething();
return x;
Der Vorteil davon besteht darin, dass, wenn der Prozessor zum ersten Mal eine Verzweigung durchführt, ein erheblicher Overhead entsteht, da er Code möglicherweise weiter vorne spekulativ geladen und ausgeführt hat. Wenn es feststellt, dass es die Verzweigung nehmen wird, muss es diese ungültig machen und beim Verzweigungsziel beginnen.
Die meisten modernen Prozessoren verfügen jetzt über eine Art Verzweigungsvorhersage, aber das hilft nur, wenn Sie die Verzweigung zuvor durchlaufen haben und sich die Verzweigung noch im Verzweigungsvorhersage-Cache befindet.
Es gibt eine Reihe anderer Strategien, die der Compiler und der Prozessor in diesen Szenarios verwenden können. Weitere Einzelheiten zur Funktionsweise von Verzweigungsprädiktoren finden Sie auf Wikipedia:http://en.wikipedia.org/wiki/Branch_predictor