E ' apparentemente funziona sia per le dichiarazioni ternarie che per quelle regolari.
In primo luogo, diamo un'occhiata ai seguenti tre esempi di codice, due dei quali usano __builtin_expect
in entrambi gli stili regular-if e ternary-if e un terzo che non lo usa affatto.
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);
}
Quando viene compilato con -O3
, tutti e tre risultato nello stesso assieme. Tuttavia, quando il -O
è lasciato fuori (su GCC 4.7.2), sia ternary.c e builtin.c hanno la stessa lista di montaggio (dove conta):
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
Mentre nobuiltin.c non:
.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
La parte rilevante:
In sostanza, __builtin_expect
cause codice aggiuntivo (sete %al
...) da eseguire prima della je .L2
sulla base dei risultati di testl %eax, %eax
che è più probabile di predire come 1 (ipotesi ingenua, qui) la CPU invece di basarsi sul confronto diretto del char di input con 'c'
. Considerando che nel caso nobuiltin.c, non esiste un tale codice e lo je
/jne
segue direttamente il confronto con 'c' (cmp $99
).Ricorda che la previsione delle diramazioni avviene principalmente nella CPU, e qui GCC sta semplicemente "ponendo una trappola" affinché il predittore del ramo CPU assuma quale percorso verrà preso (tramite il codice aggiuntivo e la commutazione di je
e jne
, sebbene io non avere una fonte per questo, come Intel official optimization manual non menziona il trattamento di primi incontri con vs jne
in modo diverso per la previsione di ramo! Posso solo supporre che il team GCC è arrivato a questo via tentativi ed errori).
Sono sicuro che ci sono casi di test migliori in cui la previsione delle branch di GCC può essere vista più direttamente (invece di osservare i suggerimenti per la CPU), anche se non so come emulare un caso in modo sintetico/conciso. (Indovina: probabilmente comporterà lo srotolamento del loop durante la compilazione.)
Analisi molto bella e presentazione molto bella dei risultati. Grazie per lo sforzo. –
Questo non mostra nulla di diverso dal fatto che '__builtin_expect' non ha effetto sul codice ottimizzato per x86 (dato che hai detto che erano uguali a -O3). L'unica ragione per cui sono diversi prima è che '__builtin_expect' è una funzione che restituisce il valore dato ad esso, e che il valore di ritorno non può avvenire attraverso i flag. Altrimenti, la differenza rimarrebbe nel codice ottimizzato. – ughoavgfhw
@ughoavgfhw: Cosa intendi per "che il valore restituito non può avvenire tramite le bandiere"? –