It najwyraźniej działa zarówno dla potrójnych, jak i regularnych instrukcji if.
Najpierw przyjrzyjmy się trzem przykładowym kodom, z których dwa używają __builtin_expect
zarówno w zwykłym, jak iw trójskładnym stylu if oraz trzecim, który w ogóle go nie używa.
builtin.c:
int main()
{
char c = getchar();
const char *printVal;
if (__builtin_expect(c == 'c', 1))
{
printVal = "Took expected branch!\n";
}
else
{
printVal = "Boo!\n";
}
printf(printVal);
}
ternary.c:
int main()
{
char c = getchar();
const char *printVal = __builtin_expect(c == 'c', 1)
? "Took expected branch!\n"
: "Boo!\n";
printf(printVal);
}
nobuiltin.c:
int main()
{
char c = getchar();
const char *printVal;
if (c == 'c')
{
printVal = "Took expected branch!\n";
}
else
{
printVal = "Boo!\n";
}
printf(printVal);
}
z wkompilowaną -O3
wszystkie trzy prowadzą do tego samego zespołu. Jednak, gdy -O
jest pominięta (na GCC 4.7.2), zarówno ternary.c i builtin.c mają taką samą listę montażową (gdzie ma to znaczenie):
builtin.s:
.file "builtin.c"
.section .rodata
.LC0:
.string "Took expected branch!\n"
.LC1:
.string "Boo!\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call getchar
movb %al, 27(%esp)
cmpb $99, 27(%esp)
sete %al
movzbl %al, %eax
testl %eax, %eax
je .L2
movl $.LC0, 28(%esp)
jmp .L3
.L2:
movl $.LC1, 28(%esp)
.L3:
movl 28(%esp), %eax
movl %eax, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Debian 4.7.2-4) 4.7.2"
.section .note.GNU-stack,"",@progbits
ternary.s:
.file "ternary.c"
.section .rodata
.LC0:
.string "Took expected branch!\n"
.LC1:
.string "Boo!\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call getchar
movb %al, 31(%esp)
cmpb $99, 31(%esp)
sete %al
movzbl %al, %eax
testl %eax, %eax
je .L2
movl $.LC0, %eax
jmp .L3
.L2:
movl $.LC1, %eax
.L3:
movl %eax, 24(%esp)
movl 24(%esp), %eax
movl %eax, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Debian 4.7.2-4) 4.7.2"
.section .note.GNU-stack,"",@progbits
Zważywszy nobuiltin.c nie:
.file "nobuiltin.c"
.section .rodata
.LC0:
.string "Took expected branch!\n"
.LC1:
.string "Boo!\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $32, %esp
call getchar
movb %al, 27(%esp)
cmpb $99, 27(%esp)
jne .L2
movl $.LC0, 28(%esp)
jmp .L3
.L2:
movl $.LC1, 28(%esp)
.L3:
movl 28(%esp), %eax
movl %eax, (%esp)
call printf
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Debian 4.7.2-4) 4.7.2"
.section .note.GNU-stack,"",@progbits
Właściwa część:
Zasadniczo __builtin_expect
przyczyny kod dodatkowy (sete %al
...) mają być wykonane przed je .L2
opartego na wynikach testl %eax, %eax
który procesor jest bardziej prawdopodobne, aby przewidzieć jako 1 (naiwne założenie, tutaj) zamiast na podstawie bezpośredniego porównania znaku wejściowego z 'c'
. Podczas gdy w przypadku nobuiltin.c żaden taki kod nie istnieje i bezpośrednio po porównaniu z "c"() ().Pamiętaj, prognozy rozgałęzień są wykonywane głównie w CPU, a tutaj GCC to po prostu "układanie pułapki" dla predyktora gałęzi CPU, aby założyć, która ścieżka zostanie pobrana (za pomocą dodatkowego kodu i przełączania je
i jne
, chociaż nie miej to źródło, ponieważ Intel official optimization manual nie wspomina o pierwszym spotkaniu z je
vs jne
inaczej dla prognozy rozgałęzień! Mogę tylko założyć, że zespół GCC doszedł do tego przez próbę i błąd).
Jestem pewien, że istnieją lepsze przypadki testowe, w których prognoza rozgałęzień GCC może być widziana bardziej bezpośrednio (zamiast obserwować podpowiedzi do procesora), chociaż nie wiem, jak naśladować taki przypadek zwięźle/zwięźle. (Zgadnij: prawdopodobnie będzie to wymagało rozwijania pętli podczas kompilacji.)
Bardzo ładna analiza i bardzo przyjemna prezentacja wyników. Dziękuję za trud. –
To naprawdę nie pokazuje niczego poza tym, że '__builtin_expect' nie ma wpływu na zoptymalizowany kod dla x86 (ponieważ powiedziałeś, że były takie same dla -O3). Jedynym powodem, dla którego różnią się one wcześniej, jest to, że '__builtin_expect' to funkcja zwracająca wartość, która została mu podana, a ta wartość zwracana nie może się zdarzyć za pośrednictwem flag. W przeciwnym razie różnica pozostanie w zoptymalizowanym kodzie. – ughoavgfhw
@ughoavgfhw: Co rozumiesz przez "że wartość zwrotu nie może się zdarzyć przez flagi"? –