分支可能性提示是否通过函数调用进行?
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;
}
}
此函数 f
将 x
和 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 )
我遇到过几个场景,我想说函数的 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;
}
}
此函数 f
将 x
和 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 )