【问题标题】:Do branch likelihood hints carry through function calls?分支可能性提示是否通过函数调用进行?
【发布时间】:2021-01-24 22:27:51
【问题描述】:

我遇到过一些场景,我想说一个函数的返回值很可能在函数体中,而不是调用它的 if 语句。

例如,假设我想将代码从使用 LIKELY 宏移植到使用新的 [[likely]] 注释。但这些在语法上不同的地方:

#define LIKELY(...) __builtin_expect(!!(__VA_ARGS__),0)
if(LIKELY(x)) { ... } 

对

if(x) [[likely]] { ... }

没有简单的方法可以重新定义 LIKELY 宏以使用注释。会定义一个类似的函数

inline bool likely(bool x) { 
  if(x) [[likely]] return true;
  else return false;
}

将提示传播到 if?喜欢在

if(likely(x)) { ... }

同样,在通用代码中,在实际的if 语句中直接表达算法似然性信息可能很困难,即使该信息在其他地方是已知的。例如,copy_if 谓词几乎总是为假。据我所知,没有办法用属性来表达,但是如果分支权重信息可以通过函数传播,这是一个已解决的问题。

到目前为止,我还没有找到有关此的文档,并且我不知道通过查看输出的程序集来测试它的良好设置。

【问题讨论】:

  • 这可以完成似乎是合乎逻辑的。那么问题是任何给定的编译器是否真的足够聪明来做到这一点。你有什么特别的编译器吗?当然,就标准而言,没有编译器有义务对提示做任何事情。
  • @NateEldredge 就我个人而言,我对 clang 和 gcc 最感兴趣,但了解更多永远不会受到伤害。
  • 编译器在内联后跟踪某物的“预期”值当然是合理的。特别是 GCC,因为原始 __builtin 的语义是为变量提供预期值,而不是特定于在分支中使用它。 (正如 Nate 所示,GCC 会这样做,但会发出叮当声主干 seems not to)
  • 事后看来,当然,最好将宏最初定义为#define LIKELY(x) (__builtin_expect(!!(x),0)) 并将其用作if LIKELY(x) { ... }。这样,移植就很容易了。 (或者甚至可以定义一个 if_likely(x) 宏,并将 if 关键字移到宏定义中。)

标签: c++ compiler-optimization c++20 branch-prediction


【解决方案1】:

对于不同的编译器来说,故事似乎是复杂的。

在 GCC 上,我认为您的内联 likely 函数有效,或者至少有一些效果。使用 Compiler Explorer 测试此代码的差异:

inline bool likely(bool x) { 
  if(x) [[likely]] return true;
  else return false;
}

//#define LIKELY(x) likely(x)
#define LIKELY(x) x

int f(int x) {
    if (LIKELY(!x)) {
        return -3548;
    }
    else {
        return x + 1;
    }
}

这个函数f 将1 加到x 并返回它,除非x 是0,在这种情况下它返回-3548。 LIKELY 宏在激活时向编译器指示x 为零的情况更为常见。

这个版本没有任何变化,在 GCC 10 -O1 下生成这个程序集:

f(int):
        test    edi, edi
        je      .L3
        lea     eax, [rdi+1]
        ret
.L3:
        mov     eax, -3548
        ret

将#define 更改为带有[[likely]] 的内联函数,我们得到:

f(int):
        lea     eax, [rdi+1]
        test    edi, edi
        mov     edx, -3548
        cmove   eax, edx
        ret

这是条件移动而不是条件跳转。我猜是一场胜利,尽管只是一个简单的例子。

这表明分支权重通过内联函数传播,这是有道理的。

然而,根据@Peter Cordes 的报告,在 clang 上,对可能和不太可能的属性的支持有限,而且似乎没有通过内联函数调用传播。

不过,我认为有一个 hacky 宏解决方案也可以:

#define EMPTY()
#define LIKELY(x) x) [[likely]] EMPTY(

然后是这样的

if ( LIKELY(x) ) {

变得像

if ( x) [[likely]] EMPTY( ) {

然后变成

if ( x) [[likely]] {

.

示例:https://godbolt.org/z/nhfehn

但请注意,这可能仅适用于 if 语句,或者在 LIKELY 括在括号中的其他情况下。

【讨论】:

  • 看起来您使用最近的 GCC 进行了测试。您忘记在答案中说明哪个编译器,或者包含一个神螺栓链接 (godbolt.org/z/c6Mr54)。 clang 与这里的 GCC 不同。此外,奇怪的是,当“已知”一种方式很可能时,GCC 会生成无分支代码;这将是打破数据对输入的依赖的好机会。它选择 not 进行 if 转换到 cmov,即使在 -O3 没有提示。此外,如果条件是x 而不是!x,则它使用cmp/sbb/and/sub 非常值得怀疑的技巧:RAX 上的错误dep,没有ILP,更长的dep 链。
  • @Peter Cordes 是的,我同意有条件的移动不是默认设置很奇怪。然而,这仍然表明 [[likely]] 通过内联函数传播,这是重要的一点。 GCC 现在已经有了 30 年的发展,这是一个非常简单的例子,几乎没有人为,所以在这种情况下,我们忽略了条件分支上的 cmove 似乎有一些隐藏的成本。当我们告诉 GCC x == 0 时进行 cmove 的事实更有可能表明此成本主要发生在 x != 0 时,即未采用分支或 cmove 时。
  • 不要过度解读 GCC 的决定。这是一个复杂的机器,但不是“智能”,并且没有人类水平的上下文理解,并且小的错过优化错误很常见(例如,只需查看 x 而不是 !x 输出,或分析IACA 或 LLVM-MCA)。如果这是循环的一部分,或者我们实际上可以讨论是否存在循环携带的依赖链,以及数据依赖是否重要,这可能会更有意义。 cmovz 是 AMD 和 Intel Broadwell 及更高版本上的单指令。 (早期英特尔上的 2 个)
  • @PeterCordes 我用宏解决方案在我的答案中添加了第二部分。
【解决方案2】:

gcc 10.2 至少可以进行这个推断(使用-O2)。

如果我们考虑以下简单的程序:

void foo();
void bar();

void baz(int x) {
    if (x == 0)
        foo();
    else
        bar();
}

然后compiles to:

baz(int):
        test    edi, edi
        jne     .L2
        jmp     foo()
.L2:
        jmp     bar()

但是,如果我们在else 子句上添加[[likely]],生成的代码changes to

baz(int):
        test    edi, edi
        je      .L4
        jmp     bar()
.L4:
        jmp     foo()

使得条件分支的未采取的情况对应于“可能”的情况。

现在,如果我们将比较结果放到一个内联函数中:

void foo();
void bar();

inline bool is_zero(int x) {
    if (x == 0)
        return true;
    else
        return false;
}

void baz(int x) {
    if (is_zero(x))
        foo();
    else
        bar();
}

我们又是back to the original generated code,在bar() 案例中采用分支。但是如果我们在is_zero 的else 子句上添加[[likely]],我们就会see the branch reversed again。

但是,clang 10.0.1 并未展示此行为,并且似乎在此示例的所有版本中完全忽略了 [[likely]]。

【讨论】:

  • clang 10.1 说:warning: unknown attribute 'likely' ignored [-Wunknown-attributes],难怪它什么也没做。 :/ Clang trunk 支持它,但不通过内联传播它。 (如果您在baz 中使用[[likely]] 与[[unlikely]] 标记if,则确实会看到效果:godbolt.org/z/EsW5xY)
【解决方案3】:

是的,它可能会内联,但这毫无意义。

即使您升级到支持这些 C++ 20 属性的编译器,__builtin_expect 仍将继续工作。您可以稍后重构它们,但这纯粹是出于审美原因。

另外,您对LIKELY 宏的实现是错误的(实际上是UNLIKELY),正确的实现如下。

#define LIKELY( x )   __builtin_expect( !! ( x ), 1 )
#define UNLIKELY( x ) __builtin_expect( !! ( x ), 0 )

【讨论】:

  • __builtin_expect 需要 GNU 扩展; ISO C++20 [[likely]] 没有,因此甚至可以在可移植代码中使用(包括 MSVC)。但因指出 OP 宏中的错误而受到支持!
  • 在通用代码的情况下,实际条件和可能性信息没有在同一个地方定义?
  • 我还修复了可能的实现!我相信它需要是可变参数的,这样预处理器才不会对具有多个参数的模板犹豫不决。
  • @Riley __builtin_expect_with_probability 中的“可能性信息”很少见,其准确性和实用性始终存在疑问。如果它存在,最好在同一个地方定义。信息的位置很重要。
  • @Riley 啊,这就是你制作宏变量的原因。你不需要这样做。如果您想将该多元模板放入宏中,您可以将包含该模板的表达式括在一组额外的括号中。这样,里面的逗号就不会被解释为宏的单独参数。不要为显然不是可变参数的东西编写可变参数宏。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-01
  • 2011-06-07
  • 2011-12-25
  • 1970-01-01
  • 2022-11-07
相关资源
最近更新 更多