分支概率提示是否在函数调用中传递?

25

我遇到了几种情况,想要表达一个函数的返回值可能在函数体内,而不是调用它的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。据我所知,没有办法使用属性来表达这一点,但如果分支权重信息可以通过函数进行传播,则问题得到解决。

到目前为止,我还没有找到相关的文档,并且我不知道如何设置好环境,以便查看输出汇编代码并对此进行测试。


2
这似乎是有道理的,可能可以做到。那么问题就在于任何给定的编译器是否真的足够聪明去做到这一点。你有特定的编译器想法吗?当然,就标准而言,没有任何编译器有义务对提示进行任何操作。 - Nate Eldredge
@NateEldredge 个人而言,我最感兴趣的是clang和gcc,但了解更多从来不会有坏处。 - Riley
2
编译器在内联后跟踪某个东西的“预期”值是完全有可能的。特别是GCC,因为原始__builtin的语义是为变量提供预期值,而不是特定于在分支中使用它。(正如Nate所示,GCC确实这样做,但clang trunk似乎没有。) - Peter Cordes
回顾过去,当然最好最初就将宏定义为 #define LIKELY(x) (__builtin_expect(!!(x),0)) 并使用 if LIKELY(x) { ... }。这样,移植就会变得容易。 (或者甚至可以定义一个 if_likely(x) 宏,将 if 关键字移动到宏定义中。) - Ilmari Karonen
3个回答

15

这个故事对不同编译器似乎有所不同。

在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为0的情况更常见。

在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

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

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

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

但是,有一种繁琐的宏解决方案,我认为也能起作用:

#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括在括号中的情况。


2
看起来你使用了最近的GCC进行测试。你在回答中忘记说使用的编译器,或者包含一个godbolt链接(https://godbolt.org/z/c6Mr54)。clang在这里与GCC不同。此外,当一种方式“已知”可能时,GCC制作无分支代码很奇怪;这是打破输入数据依赖关系的好机会。即使没有提示,在-O3下它也选择*不*将if-conversion转换为`cmov`。此外,如果条件是`x`而不是`!x`,它使用了一个相当可疑的`cmp`/`sbb`/`and`/`sub`技巧:对RAX的错误依赖,没有ILP,更长的依赖链。 - Peter Cordes
@Peter Cordes,是的,我同意条件移动不是默认选项很奇怪。然而,这仍然显示了[[likely]]通过内联函数传播,这是重要的一点。GCC现在已经有30年的开发历史了,这是一个相当简单的例子,几乎没有人为的因素,所以似乎可能存在一些隐藏的成本,在这种情况下,cmove比条件分支更好。当我们告诉GCC x == 0时,进行cmove更有可能表明这个成本主要是在x != 0时,即当分支或cmove未被执行时。 - Anonymous1847
1
不要对GCC的决策过于解读。它是一台复杂的机器,但并不“聪明”,没有人类级别的上下文理解能力,小的优化错误很常见(例如只看看x而不是!x的输出,或者使用IACA或LLVM-MCA进行分析)。如果这是一个循环的一部分,或者我们可以实际讨论是否存在循环依赖链以及数据依赖关系是否重要,那么它可能会稍微有些意义。在AMD上,cmovz是单uop指令,在Intel Broadwell及更高版本上也是如此。(在早期的Intel上为2) - Peter Cordes
@PeterCordes,我在答案中添加了第二部分,提供了一个宏解决方案。 - Anonymous1847

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

然而,如果我们在else子句中添加[[likely]],生成的代码将更改为
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()的情况。但是如果在is_zeroelse子句上添加[[likely]],我们将再次看到分支被反转。然而,clang 10.0.1并没有展示这种行为,似乎在所有版本的这个例子中都完全忽略了[[likely]]

3
clang 10.1 发出警告:warning: unknown attribute 'likely' ignored [-Wunknown-attributes],所以它不会起作用。:/ Clang trunk 支持该特性,但在内联时不会传播此特性。(如果您将baz函数中的if标记为[[likely]] vs. [[unlikely]],则可以看到影响:https://godbolt.org/z/EsW5xY) - Peter Cordes

3

是的,它可能会内联,但这没什么意义。

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

此外,您对LIKELY宏的实现是错误的(实际上应该是UNLIKELY),以下是正确的实现:

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

3
__builtin_expect需要GNU扩展;ISO C++20的[[likely]]不需要,因此可以在可移植代码中使用(包括MSVC)。但是点赞以指出OP宏中的错误! - Peter Cordes
泛型代码的情况怎么样,实际条件和可能性信息不在同一个地方定义? - Riley
1
我还修复了可能的实现!我认为它需要是可变参数的,这样预处理器就不会因模板有多个参数而报错。 - Riley
@Riley 在 if 语句中允许逗号,或者在实际条件之前执行随机操作,这是一种非常糟糕的代码气味。你真的不应该鼓励这样做,特别是在宏中。 - KevinZ
1
@Riley 啊,所以你才把宏定义成可变参数的形式。其实你不需要这么做。如果你想把多元模板放在宏定义里面,你可以在包含模板的表达式外面再加一层括号。这样,内部的逗号就不会被解释为宏定义的分隔符了。不要为明显不是可变参数的东西写一个可变参数的宏定义。 - KevinZ
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接