Stellen Sie sich den Assembler-Code vor, der generiert würde aus:
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
Ich denke, es sollte so etwas wie:
sein cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
Sie können sehen, dass die Anweisungen in einer solchen Reihenfolge angeordnet sind, dass der bar
Groß-/Kleinschreibung steht vor dem foo
Fall (im Gegensatz zum C-Code). Dadurch kann die CPU-Pipeline besser ausgelastet werden, da ein Sprung die bereits geholten Befehle zertrümmert.
Bevor der Sprung ausgeführt wird, werden die Anweisungen darunter (der bar
Fall) werden in die Pipeline geschoben. Seit foo
Fall ist unwahrscheinlich, auch Sprünge sind unwahrscheinlich, daher ist es unwahrscheinlich, dass die Pipeline zerstört wird.
Lassen Sie uns dekompilieren, um zu sehen, was GCC 4.8 damit macht
Blagovest erwähnte die Verzweigungsinversion, um die Pipeline zu verbessern, aber machen aktuelle Compiler das wirklich? Finden wir es heraus!
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)
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 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
Die Befehlsreihenfolge im Speicher blieb unverändert:zuerst die puts
und dann 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 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>
Die puts
wurde an das Ende der Funktion verschoben, die retq
zurück!
Der neue Code ist im Grunde derselbe wie:
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
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.
Die Idee von __builtin_expect
ist, dem Compiler mitzuteilen, dass der Ausdruck normalerweise zu c ausgewertet wird, damit der Compiler für diesen Fall optimieren kann.
Ich würde vermuten, dass jemand dachte, er sei schlau und würde die Dinge dadurch beschleunigen.
Leider, es sei denn, die Situation ist sehr gut verstanden (es ist wahrscheinlich, dass sie so etwas nicht getan haben), es könnte die Dinge noch schlimmer gemacht haben. Die Dokumentation sagt sogar:
Im Allgemeinen sollten Sie dafür lieber das tatsächliche Profilfeedback verwenden (-fprofile-arcs
), da Programmierer notorisch schlecht darin sind, die tatsächliche Leistung ihrer Programme vorherzusagen. Es gibt jedoch Anwendungen, bei denen diese Daten schwer zu erfassen sind.
Im Allgemeinen sollten Sie __builtin_expect
nicht verwenden es sei denn:
- Sie haben ein sehr reales Leistungsproblem
- Sie haben die Algorithmen im System bereits entsprechend optimiert
- Sie haben Leistungsdaten, die Ihre Behauptung untermauern, dass ein bestimmter Fall am wahrscheinlichsten ist