GCC manual仅显示__builtin_expect()放置在'if'语句的整个条件周围的示例。
我还注意到GCC不会抱怨,如果我使用它,例如,使用三元运算符,或任何任意积分表达式,即使是在分支上下文中没有使用的。
所以,我想知道其实际使用的基本限制是什么。
在这样的三元操作中使用它是否会保持其效果:
int foo(int i)
{
return __builtin_expect(i == 7, 1) ? 100 : 200;
}
这个案子怎么样:
int foo(int i)
{
return __builtin_expect(i, 7) == 7 ? 100 : 200;
}
这一个:
int foo(int i)
{
int j = __builtin_expect(i, 7);
return j == 7 ? 100 : 200;
}
答案 0 :(得分:8)
它显然适用于三元和常规if语句。
首先,让我们看一下以下三个代码示例,其中两个代码在regular-if和ternary-if样式中使用__builtin_expect
,第三个代码样本根本不使用它。
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
的结果,CPU更有可能预测为1(天真的假设,这里)而不是基于输入字符与'c'
的直接比较。而在nobuiltin.c案例中,不存在此类代码,而je
/ jne
直接跟随与'c'(cmp $99
)的比较。请记住,分支预测主要在CPU中完成,这里GCC只是为CPU分支预测器“设置一个陷阱”来假设将采用哪条路径(通过额外的代码以及je
和{{的切换1}},虽然我没有这方面的来源,因为英特尔的official optimization manual没有提到以jne
对je
对分支预测的不同处理首次遭遇!我只能假设海湾合作委员会小组通过反复试验来到这里。
我确信有更好的测试用例可以更直接地看到GCC的分支预测(而不是观察CPU的提示),尽管我不知道如何简洁/简洁地模拟这样的情况。 (猜猜:在编译过程中可能会涉及循环展开。)