【问题标题】:Is it reasonable to mark only part of an expression as likely()/unlikely()仅将表达式的一部分标记为可能()/不太可能()是否合理
【发布时间】:2017-01-29 16:30:12
【问题描述】:

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

if (could_be || very_improbable) {
    DoSomething();
}

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

if (could_be || unlikely(very_improbable)) {
    DoSomething();
}

注意:我不是在问 marcos 是如何工作的——我明白这一点。这里的问题是关于 GCC 的,如果我只暗示它的一部分,它是否能够优化表达式。我也认识到它可能在很大程度上取决于所讨论的表达式 - 我正在吸引那些对这些宏有经验的人。

【问题讨论】:

  • 如果您要求 GCC(例如,-Wall -O2 -march=x86-64 -mtune=generic -S,为通用 AMD64/x86-64 架构生成程序集),至少 GCC-5.4 说“不”;生成的代码没有区别。
  • @NominalAnimal 你的意思是你创建了一个代码sn-p并编译了它?
  • @immortal:是的,使用void func(int neutral, int rare) { if (neutral || rare) other(); }。通常__builtin_expect(expr, 1) (likely(expr)) 和__builtin_expect(expr, 0) (unlikely(expr)) 有助于分支预测,但在这种情况下,我们最终得到一个分支(好像使用了if (neutral | rare)),因为表达式很简单.对于复杂的表达式,一切都取决于表达式本身;一般来说,只有在分析表明编译器在这一点上一直选择错误之后才应该添加宏。其他任何事情都是过早的优化。

标签: c gcc optimization


【解决方案1】:

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

在您的实际示例中,如果 could_bevery_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();
}

这里的谓词由两个带参数的f() 调用组成,icclikelyunlikely 的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() 情况是失败的。

所以编译器绝对可以使用精确的likelyunlikely 信息,这确实是有道理的:如果你把上面的测试分成两个链接的if 语句,很明显单独的分支提示会起作用,所以毫不奇怪,&& 的语义等价用法仍然受益于提示。

以下是其他一些没有得到“全文”处理的笔记,以防你走到这一步:

  • 我使用icc 来说明示例,但对于此测试,至少clanggcc 都进行了相同的基本优化(不同地编译4 个案例中的3 个)。
  • 编译器可以通过知道子谓词的概率进行的一个“明显”优化是颠倒谓词的顺序。例如,如果您有likely(X) && unlikely(Y),您可以先检查条件Y,因为它很可能允许您快捷检查Y1。显然,gcc 可以make this optimization 用于简单的谓词,但我无法哄骗iccclang 这样做。 gcc 优化显然非常脆弱:disappears 如果您稍微更改谓词,即使在这种情况下优化会好得多
  • 当编译器不能保证转换后的代码会表现得“好像”它是根据语言语义直接编译的一样时,它们会被限制进行优化。特别是,除非他们能证明这些操作没有副作用,否则他们对操作重新排序的能力有限。在构建谓词时请记住这一点。

1当然,这只有在编译器可以看到XY没有副作用的情况下才允许,也可能不是有效如果 YX 相比检查成本要高得多(因为避免检查 Y 的任何好处都被额外的 X 评估的高成本所压倒)。

【讨论】:

  • 感谢您的全面解答!只是为了彻底解决问题-您使用/省略了哪些优化标志来使编译器(我与 GCC 合作)产生预期的效果?我猜 O2 会很明显,你认为 O3 还会这样做吗?
【解决方案2】:

是的,这可能有帮助。例如在下面的例子中,当XXX 关闭时,GCC 将测试x before 意外y > 0 从而避免在运行时执行不太可能的代码(Cygwin,gcc 5.4)。当然,在这种特殊情况下,y check 是在 x check 之前编写的,但这并不重要,因为在代码生成期间,您的代码可能会以不可预知的方式被 GCC 重新洗牌。

#ifdef XXX
#define __builtin_expect(x, y) (x)
#endif

void bar();

void foo(int x, int y, int z) {
  if(__builtin_expect(y > 0, 0) || x == 0)
    bar ();
}

XXX 关闭时(即__builtin_expect 处于活动状态):

foo:
  testl   %ecx, %ecx
  je      .L4
  testl   %edx, %edx
  jg      .L4
  rep ret

XXX 被定义时(即__builtin_expect 被忽略):

foo:
  testl   %edx, %edx
  jg      .L4
  testl   %ecx, %ecx
  je      .L4
  rep ret

【讨论】:

  • 好收获。我找不到 gcc 实际上重新排序支票的案例,但显然这是一个。
【解决方案3】:

是的,C 使用智能或守卫。所以如果我们写

  if(a() || b())

然后评估 a。如果为真,则不评估 b。如果为 false,当然必须对 b 进行评估才能做出最终决定。

因此,如果 a() 便宜或可能,b() 昂贵或不可能,则将 a() 放在首位是值得的。但是代码相当于

  if(a())
  {
      do_it();
  }
  /* action point */
  else if(b())
  {
     do_it();
  }

一旦控制流到达“action_point”,您就会看到告诉编译器是否可能跳转可能会有所帮助。

【讨论】:

  • “短路”是“智能或保护”的专有名词。
  • 我还要提到a()b() 必须没有副作用。目前,它们看起来像函数调用,通常不能重新排序,而不管__builtin_expect 注释如何。
  • 这回答了一个不同的问题:如何最好地排序短路运算符的条件,据我了解,它与likelyunlikely 并没有直接关系。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多