仅将表达式的一部分标记为可能()/不可能()是否合理

imm*_*tal 10 c optimization gcc

假设我有一个表达式,其中只有一部分是不太可能的,但另一个是统计中性的:

if (could_be || very_improbable) {
    DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

如果我将非常不可能的位放在unlikely()宏中,它会以任何方式帮助编译器吗?

if (could_be || unlikely(very_improbable)) {
    DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

注意:我不是在问马科斯是如何工作的 - 我理解这一点.这里的问题是关于GCC,如果我只暗示其中的一部分,它是否能够优化表达式.我也认识到它可能在很大程度上取决于所讨论的表达方式 - 我对那些有这些宏经验的人很有吸引力.

Bee*_*ope 6

是的,这是合理的,编译器可以并且确实在正确的场景中利用它.

在您的实际示例中,如果could_be并且very_improbable实际上是整数变量,那么在谓词的子表达式上插入likelyunlikely宏将不会有任何意义,因为编译器可以做些什么才能使其更快?编译器可以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();
}
Run Code Online (Sandbox Code Playgroud)

这里的谓词是由两个呼叫的到f()与参数,和icc3产生不同的代码出的4种组合的likelyunlikely:

代码产生likely(f(a)) && likely(f(b)):

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
Run Code Online (Sandbox Code Playgroud)

在这里,两个谓词都可能是正确的,因此icc在两者都为真的情况下产生直线代码,如果结果为假,则跳出线外.

代码产生unlikely(f(a)) && likely(f(b)):

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
Run Code Online (Sandbox Code Playgroud)

现在,谓词很可能是假的,因此icc产生直线代码,在这种情况下直接导致返回,并跳出行B1.5以继续谓词.在这种情况下,预计第二呼叫(f(b))是真实的,因此它通过在结束码生成下降尾部调用fn1().如果第二个调用结果为false,则它会跳回到已经为第一个跳转(标签B1.3)中的掉落情况组装的相同序列.

也是生成的代码unlikely(f(a)) && unlikely(f(b)).在这种情况下,您可以想象编译器更改代码的结尾以将a jmp fn2()作为连接案例,但事实并非如此.值得注意的是,这会阻止重复使用前面的序列,B1.3并且我们甚至不太可能执行此代码,因此,优化较小的代码大小优于已经不太可能的情况似乎是合理的.

代码产生likely(f(a)) && unlikely(f(b)):

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
Run Code Online (Sandbox Code Playgroud)

这类似于第一个case(likely && likely),除了第二个谓词的期望现在是假的,因此它重新排序块,以便return fn2()情况是直通的.

因此编译器肯定可以使用精确likelyunlikely信息,实际上它是有道理的:如果你将上面的测试分解为两个链式if语句,很明显单独的分支提示可以工作,所以在语义上等效使用&&仍然不足为奇提示的好处.

这里有一些其他注意事项没有得到"全文"处理,以防你这么做:

  • 我曾经icc举例说明了这些例子,但是对于这个测试至少两个clanggcc进行相同的基本优化(以不同方式编译4个案例中的3个).
  • 编译器通过了解子谓词的概率可以做出的一个"明显的"优化是颠倒谓词的顺序.例如,如果你有likely(X) && unlikely(Y),你可以先检查条件Y,因为它很可能允许你快捷检查Y 1.显然,gcc可以对简单谓词进行优化,但是我无法哄骗iccclang这样做.gcc优化显然非常脆弱:如果稍微更改谓词,它就会消失,即使在这种情况下优化会更好.
  • 当编译器不能保证转换后的代码将"好像"根据语言语义直接编译时,就不能进行优化.特别是,他们对操作重新排序的能力有限,除非他们能证明操作没有副作用.在构造谓词时请记住这一点.

1.当然,这只是允许当编译器可以看到,XY没有任何副作用,它可能不是有效的,如果Y是贵得多相比,检查X(因为任何避免检查的好处Y是由高不堪重负额外X评估的费用).