Ответ 1
Он, по-видимому, работает как для тернарных, так и для регулярных операторов if.
Во-первых, рассмотрим следующие три примера кода, два из которых используют __builtin_expect
в стилях обычного-if и trernary-if, а третий, который не использует его вообще.
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);
}
При компиляции с -O3
все три результата приводятся к одной и той же сборке. Однако, когда значение -O
отсутствует (в GCC 4.7.2), оба ternary.c и builtin.c имеют один и тот же список сборок (где это важно):
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
В то время как nobuiltin.c не делает:
.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
Соответствующая часть:
В основном, __builtin_expect
вызывает дополнительный код (sete %al
...), который должен быть выполнен до je .L2
на основе результата testl %eax, %eax
, который процессор с большей вероятностью прогнозирует как 1 (наивное предположение, здесь) вместо того, чтобы основываться на прямом сравнении ввода char с 'c'
. В то время как в случае nobuiltin.c такой код не существует, а je
/jne
непосредственно следует за сравнением с 'c' (cmp $99
). Помните, что предсказание ветвлений в основном выполняется в CPU, и здесь GCC просто "кладет ловушку" для предсказателя ветвления процессора, чтобы предположить, какой путь будет выполнен (через дополнительный код и переключение je
и jne
, хотя у меня нет источника для этого, так как официальное руководство по оптимизации Intel не упоминает о лечении первых встречей с помощью je
vs jne
по-разному для предсказания ветки! Я могу только предположить, что команда GCC прибыла на это через пробную версию и ошибку).
Я уверен, что есть лучшие тестовые примеры, когда предсказание ветвей GCC можно увидеть более непосредственно (вместо того, чтобы наблюдать намеки на CPU), хотя я не знаю, как эмулировать такой случай лаконично/кратко. (Угадайте: скорее всего, он включит цикл во время компиляции.)