Představte si kód sestavení, který by byl vygenerován z:
if (__builtin_expect(x, 0)) {
foo();
...
} else {
bar();
...
}
Myslím, že by to mělo být něco jako:
cmp $x, 0
jne _foo
_bar:
call bar
...
jmp after_if
_foo:
call foo
...
after_if:
Můžete vidět, že instrukce jsou uspořádány v takovém pořadí, že bar
malá a velká písmena předchází foo
pouzdro (na rozdíl od kódu C). To může lépe využít kanál CPU, protože skok zničí již načtené instrukce.
Před provedením skoku se zobrazí instrukce pod ním (bar
pouzdro) jsou tlačeny do potrubí. Od foo
případ je nepravděpodobný, skákání je také nepravděpodobné, proto je nepravděpodobné rozbití potrubí.
Pojďme provést dekompilaci, abychom viděli, co s tím GCC 4.8 dělá
Blagovest zmínil inverzi větví, aby zlepšil pipeline, ale skutečně to dělají současné kompilátory? Pojďme to zjistit!
Bez __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;
}
Kompilace a dekompilace pomocí GCC 4.8.2 x86_64 Linux:
gcc -c -O3 -std=gnu11 main.c
objdump -dr main.o
Výstup:
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
Pořadí instrukcí v paměti bylo nezměněno:nejprve puts
a poté retq
návrat.
S __builtin_expect
Nyní nahraďte if (i)
s:
if (__builtin_expect(i, 0))
a dostaneme:
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>
puts
byl přesunut na úplný konec funkce, retq
vraťte se!
Nový kód je v podstatě stejný jako:
int i = !time(NULL);
if (i)
goto puts;
ret:
return 0;
puts:
puts("a");
goto ret;
Tato optimalizace nebyla provedena pomocí -O0
.
Ale hodně štěstí při psaní příkladu, který běží rychleji s __builtin_expect
než bez, CPU jsou v té době opravdu chytré. Moje naivní pokusy jsou tady.
C++20 [[likely]]
a [[unlikely]]
C++20 standardizovalo tyto vestavěné prvky C++:Jak použít atribut pravděpodobné/nepravděpodobné C++20 v příkazu if-else Pravděpodobně (slovní hříčka!) udělají totéž.
Myšlenka __builtin_expect
je sdělit kompilátoru, že obvykle zjistíte, že výraz je vyhodnocen jako c, takže kompilátor může pro tento případ optimalizovat.
Hádám, že si někdo myslel, že jsou chytří a že tím věci urychlují.
Bohužel, pokud není situace velmi dobře pochopena (je pravděpodobné, že nic takového neudělali), možná to všechno ještě zhoršilo. Dokumentace dokonce říká:
Obecně byste k tomu měli raději použít skutečnou zpětnou vazbu od profilu (
-fprofile-arcs
), protože programátoři jsou notoricky špatní v předpovídání toho, jak jejich programy skutečně fungují. Existují však aplikace, ve kterých je obtížné tato data sbírat.
Obecně byste neměli používat __builtin_expect
pokud:
- Máte velmi skutečný problém s výkonem
- Algoritmy v systému jste již náležitě optimalizovali
- Máte údaje o výkonu, které podpoří vaše tvrzení, že nejpravděpodobnější je konkrétní případ