分支可能性提示是否通过函数调用进行?

Do branch likelihood hints carry through function calls?

我遇到过几个场景,我想说函数的 return 值可能在函数体内,而不是调用它的 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 中的谓词几乎总是假的。据我所知,没有办法用属性来表达,但如果分支权重信息可以通过函数传播,这是一个解决的问题。

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

对于不同的编译器,这个故事似乎是混合的。

在 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;
    }
}

此函数 fx 和 returns 加 1,除非 x 为 0,在这种情况下它 returns -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 上,对 likely 和 unlikely 属性的支持有限,并且在有的地方它似乎不会通过内联函数调用传播。

然而,有一个我认为也有效的 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-statements,或者在 LIKELY 包含在括号中的其他情况下。

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()

因此条件分支的 not-taken 情况对应于“可能”的情况。

现在,如果我们将比较提取到内联函数中:

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, taking the branch in the bar() case. But if we add [[likely]] on the else clause in is_zero, we see the branch reversed again.

然而,

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

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

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

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

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