是的,这是合理的,编译器可以并且确实在正确的场景中利用它。
在您的实际示例中,如果 could_be 和 very_improbable 实际上是整数变量,则在谓词的子表达式上插入 likely 或 unlikely 宏没有任何意义,因为编译器真的可以做些什么来让它更快?编译器可以根据分支的可能结果以不同方式组织 if 块,但仅仅因为 very_improbably 不太可能没有帮助:它仍然需要生成代码来测试它。
让我们举一个编译器可以做更多工作的例子:
extern int fn1();
extern int fn2();
extern int f(int x);
int test_likely(int a, int b) {
if (likely(f(a)) && unlikely(f(b)))
return fn1();
return fn2();
}
这里的谓词由两个带参数的f() 调用组成,icc 为likely 和unlikely 的4 种组合中的3 种产生不同的代码:
likely(f(a)) && likely(f(b))的代码produced:
test_likely(int, int):
push r15 #8.31
mov r15d, esi #8.31
call f(int) #9.7
test eax, eax #9.7
je ..B1.7 # Prob 5% #9.7
mov edi, r15d #9.23
call f(int) #9.23
test eax, eax #9.23
je ..B1.7 # Prob 5% #9.23
pop r15 #10.12
jmp fn1() #10.12
..B1.7: # Preds ..B1.4 ..B1.2
pop r15 #11.10
jmp fn2() #11.10
这里,两个谓词都可能为真,因此icc 会在两者都为真的情况下生成直线代码,如果其中一个为假,则会跳出线。
unlikely(f(a)) && likely(f(b))的代码produced:
test_likely(int, int):
push r15 #8.31
mov r15d, esi #8.31
call f(int) #9.7
test eax, eax #9.7
jne ..B1.5 # Prob 5% #9.7
..B1.3: # Preds ..B1.6 ..B1.2
pop r15 #11.10
jmp fn2() #11.10
..B1.5: # Preds ..B1.2
mov edi, r15d #9.25
call f(int) #9.25
test eax, eax #9.25
je ..B1.3 # Prob 5% #9.25
pop r15 #10.12
jmp fn1() #10.12
现在,谓词很可能是假的,所以icc 在这种情况下产生直接导致返回的直线代码,并跳出线到B1.5 以继续谓词。在这种情况下,它希望第二次调用 (f(b)) 为真,因此它会通过以tail-call 结尾的代码生成跌倒到fn1()。如果第二次调用结果为假,它会跳回到已经为第一次跳转中的失败案例组装的相同序列(标签B1.3)。
原来也是为unlikely(f(a)) && unlikely(f(b))生成的代码。在这种情况下,您可以想象编译器更改代码的结尾以将 jmp fn2() 作为失败案例,但事实并非如此。关键是要注意,这将阻止在 B1.3 处重复使用早期序列,而且我们甚至执行此代码也不太可能,因此倾向于更小的代码大小似乎是合理的优化已经不太可能的情况。
likely(f(a)) && unlikely(f(b))的代码produced:
test_likely(int, int):
push r15 #8.31
mov r15d, esi #8.31
call f(int) #9.7
test eax, eax #9.7
je ..B1.5 # Prob 5% #9.7
mov edi, r15d #9.23
call f(int) #9.23
test eax, eax #9.23
jne ..B1.7 # Prob 5% #9.23
..B1.5: # Preds ..B1.4 ..B1.2
pop r15 #11.10
jmp fn2() #11.10
..B1.7: # Preds ..B1.4
pop r15 #10.12
jmp fn1() #10.12
这与第一种情况 (likely && likely) 类似,只是现在对第二个谓词的期望为假,因此它对块进行重新排序,以便 return fn2() 情况是失败的。
所以编译器绝对可以使用精确的likely 和unlikely 信息,这确实是有道理的:如果你把上面的测试分成两个链接的if 语句,很明显单独的分支提示会起作用,所以毫不奇怪,&& 的语义等价用法仍然受益于提示。
以下是其他一些没有得到“全文”处理的笔记,以防你走到这一步:
- 我使用
icc 来说明示例,但对于此测试,至少clang 和gcc 都进行了相同的基本优化(不同地编译4 个案例中的3 个)。
- 编译器可以通过知道子谓词的概率进行的一个“明显”优化是颠倒谓词的顺序。例如,如果您有
likely(X) && unlikely(Y),您可以先检查条件Y,因为它很可能允许您快捷检查Y1。显然,gcc 可以make this optimization 用于简单的谓词,但我无法哄骗icc 或clang 这样做。 gcc 优化显然非常脆弱:disappears 如果您稍微更改谓词,即使在这种情况下优化会好得多。
- 当编译器不能保证转换后的代码会表现得“好像”它是根据语言语义直接编译的一样时,它们会被限制进行优化。特别是,除非他们能证明这些操作没有副作用,否则他们对操作重新排序的能力有限。在构建谓词时请记住这一点。
1当然,这只有在编译器可以看到X和Y没有副作用的情况下才允许,也可能不是有效如果 Y 与 X 相比检查成本要高得多(因为避免检查 Y 的任何好处都被额外的 X 评估的高成本所压倒)。