22

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

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

4

3 回答 3

14

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

在 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加 1x并返回它,除非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 括在括号中的其他情况下。

于 2020-10-09T21:25:32.190 回答
5

gcc 10.2 至少能够进行此扣除(使用-O2)。

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

void foo();
void bar();

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

然后它编译为

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

但是,如果我们添加[[likely]]else子句,生成的代码将更改为

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

我们再次回到原来生成的代码,取分支中的bar()案例。但是如果我们添加[[likely]]inelse子句is_zero,我们会看到分支再次反转

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

于 2020-10-09T21:29:37.290 回答
3

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

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

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

#define LIKELY( x )   __builtin_expect( !! ( x ), 1 )
#define UNLIKELY( x ) __builtin_expect( !! ( x ), 0 )
于 2020-10-11T17:32:53.647 回答