Es funktioniert offenbar sowohl für ternäre als auch für regelmäßige if-Anweisungen.
Zunächst sehen wir uns die folgenden drei Codebeispiele an, von denen zwei __builtin_expect
sowohl im regulären als auch im ternären if-Stil verwenden, und ein dritter, der es überhaupt nicht verwendet.
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);
}
Wenn mit -O3
zusammengestellt, die alle drei Ergebnisse in der gleichen Anordnung. Wenn jedoch die -O
weggelassen wird (auf GCC 4.7.2), die beide ternary.c und builtin.c haben die gleiche Assembler-Liste (wo es wichtig ist):
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
Während nobuiltin.c nicht der Fall ist:
.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
Der relevante Teil:
Grundsätzlich __builtin_expect
Ursachen zusätzlicher Code (sete %al
...) vor den auf dem Ergebnis der testl %eax, %eax
basierend je .L2
ausgeführt werden, die die CPU wahrscheinlicher ist als eine vorherzusagen (naive Annahme, hier) anstatt basierend auf dem direkten Vergleich der Eingabe char mit 'c'
. Während im nobuiltin.c-Fall kein solcher Code existiert und der je
/jne
direkt dem Vergleich mit 'c' folgt (cmp $99
).Denken Sie daran, Verzweigungsvorhersage wird hauptsächlich in der CPU durchgeführt, und hier legt GCC einfach eine Falle für den CPU-Verzweigungsvorhersager, um anzunehmen, welcher Pfad genommen wird (über den zusätzlichen Code und die Umschaltung von je
und jne
, obwohl ich nicht tue habe eine Quelle dafür, da Intels official optimization manual nicht erwähnt, erste Begegnungen mit je
vs jne
anders für die Verzweigungsprognose zu behandeln! Ich kann nur annehmen, dass das GCC-Team über Versuch und Irrtum hier angekommen ist).
Ich bin mir sicher, dass es bessere Testfälle gibt, in denen GCCs Verzweigungsvorhersage direkter gesehen werden kann (anstatt Hinweise auf die CPU zu beobachten), obwohl ich nicht weiß, wie man einen solchen Fall prägnant nachbildet. (Vermutung: Es würde wahrscheinlich Schleife Abrollung während der Kompilierung beinhalten.)
Sehr schöne Analyse und sehr schöne Präsentation der Ergebnisse. Danke für die Mühe. –
Dies zeigt nicht wirklich etwas anderes als dass "__builtin_expect" keinen Einfluss auf den optimierten Code für x86 hat (da Sie sagten, dass sie mit -O3 identisch waren). Der einzige Grund, warum sie vorher anders sind, ist, dass "__builtin_expect" eine Funktion ist, die den ihr gegebenen Wert zurückgibt, und dass der Rückgabewert nicht durch Flags passieren kann. Andernfalls würde der Unterschied im optimierten Code bleiben. – ughoavgfhw
@ughoavgfhw: Was meinst du mit "dieser Rückgabewert kann nicht durch Flaggen passieren"? –