如何在没有运行时成本的情况下基于断言指导 GCC 优化?
How to guide GCC optimizations based on assertions without runtime cost?
我在我的代码中使用了一个宏,在调试模式下:
#define contract(condition) \
if (!(condition)) \
throw exception("a contract has been violated");
...但在发布模式下:
#define contract(condition) \
if (!(condition)) \
__builtin_unreachable();
这对 assert()
的作用是,在发布版本中,由于 UB 传播,编译器可以大量优化代码。
例如使用以下代码进行测试:
int foo(int i) {
contract(i == 1);
return i;
}
// ...
foo(0);
... 在调试模式下抛出异常,但在发布模式下为无条件 return 1;
生成程序集:
foo(int):
mov eax, 1
ret
条件以及依赖它的所有内容都已优化。
我的问题出现在更复杂的情况下。当编译器无法 证明 该条件没有副作用时,它不会优化它,与不使用合同相比,这是一个 runtme 惩罚。
有没有办法表达合约中的条件没有副作用,从而总是优化出来?
所以,这不是答案,而是一些有趣的结果,可能会导向某个地方。
我最终得到了以下玩具代码:
#define contract(x) \
if (![&]() __attribute__((pure, noinline)) { return (x); }()) \
__builtin_unreachable();
bool noSideEffect(int i);
int foo(int i) {
contract(noSideEffect(i));
contract(i == 1);
return i;
}
你也可以 follow along at home;)
noSideEffect
是我们知道没有副作用的函数,但编译器没有。
它是这样的:
GCC __attribute__((pure))
将函数标记为没有副作用。
使用 pure
属性限定 noSideEffect
将完全删除函数调用。不错!
但是我们不能修改noSideEffect
的声明。那么如何将它的调用包装在一个本身就是 pure
的函数中呢?由于我们正在尝试制作一个独立的宏,lambda 听起来不错。
令人惊讶的是,这不起作用...除非我们将 noinline
添加到 lambda!我假设优化器首先内联 lambda,在途中丢失 pure
属性,然后再考虑优化对 noSideEffect
的调用。使用 noinline
,以一种有点违反直觉的方式,优化器能够清除所有内容。太棒了!
但是,现在有两个问题:使用 noinline
属性,编译器会为每个 lambda 生成一个主体,即使它们从未被使用过。嗯——链接器可能无论如何都能把它们扔掉。
但更重要的是......我们实际上失去了 __builtin_unreachable()
启用的优化:(
总而言之,您可以删除或放回上面代码段中的 noinline
,并最终得到以下结果之一:
和noinline
; Unused code
foo(int)::{lambda()#2}::operator()() const:
mov rax, QWORD PTR [rdi]
cmp DWORD PTR [rax], 1
sete al
ret
foo(int)::{lambda()#1}::operator()() const:
mov rax, QWORD PTR [rdi]
mov edi, DWORD PTR [rax]
jmp noSideEffect(int)
; No function call, but the access to i is performed
foo(int):
mov eax, edi
ret
没有noinline
; No unused code
; Access to i has been optimized out,
; but the call to `noSideEffect` has been kept.
foo(int):
sub rsp, 8
call noSideEffect(int)
mov eax, 1
add rsp, 8
ret
Is there a way to express that the condition in the contract has no side effect, so that it is always optimized out?
不太可能。
众所周知,您不能收集大量断言,将它们转化为假设(通过 __builtin_unreachable
)并期望得到好的结果(例如 John Regehr 的 Assertions Are Pessimistic, Assumptions Are Optimistic)。
一些线索:
CLANG,虽然已经具有 __builtin_unreachable
内在特性,但正是为此目的引入了 __builtin_assume。
N4425 - Generalized Dynamic Assumptions(*) 指出:
GCC does not explicitly provide a general assumption facility, but general assumptions can be encoded using a combination of control flow and the __builtin_unreachable
intrinsic
...
The existing implementations that provide generic assumptions use some keyword in the implementationreserved identifier space (__assume
, __builtin_assume
, etc.). Because the expression argument is not evaluated (side effects are discarded), specifying this in terms of a special library function (e.g. std::assume
) seems difficult.
指南支持库(GSL,由 Microsoft 托管,但绝不是 Microsoft 特定的)有 "merely" 此代码:
#ifdef _MSC_VER
#define GSL_ASSUME(cond) __assume(cond)
#elif defined(__clang__)
#define GSL_ASSUME(cond) __builtin_assume(cond)
#elif defined(__GNUC__)
#define GSL_ASSUME(cond) ((cond) ? static_cast<void>(0) : __builtin_unreachable())
#else
#define GSL_ASSUME(cond) static_cast<void>(!!(cond))
#endif
并指出:
// GSL_ASSUME(cond)
//
// Tell the optimizer that the predicate cond must hold. It is unspecified
// whether or not cond is actually evaluated.
*) 文件 rejected:EWG 的指南是在拟议的合同设施内提供功能。
没有办法像死代码一样强制优化代码,因为 GCC 必须始终符合标准。
另一方面,可以使用属性 error
检查表达式是否没有任何副作用,只要无法优化函数调用,它就会显示错误。
一个宏示例,用于检查已优化的内容并执行 UB 传播:
#define _contract(condition) \
{
([&]() __attribute__ ((noinline,error ("contract could not be optimized out"))) {
if (condition) {} // using the condition in if seem to hide `unused` warnings.
}());
if (!(condition))
__builtin_unreachable();
}
error属性在没有优化的情况下不起作用(所以这个宏只能用于release\optimization模式编译)。
请注意,指示合同有副作用的错误会在链接期间显示。
A test that shows an error with unoptimizable contract.
A test that optimizes out a contract but, does UB propagation with it.
我在我的代码中使用了一个宏,在调试模式下:
#define contract(condition) \
if (!(condition)) \
throw exception("a contract has been violated");
...但在发布模式下:
#define contract(condition) \
if (!(condition)) \
__builtin_unreachable();
这对 assert()
的作用是,在发布版本中,由于 UB 传播,编译器可以大量优化代码。
例如使用以下代码进行测试:
int foo(int i) {
contract(i == 1);
return i;
}
// ...
foo(0);
... 在调试模式下抛出异常,但在发布模式下为无条件 return 1;
生成程序集:
foo(int):
mov eax, 1
ret
条件以及依赖它的所有内容都已优化。
我的问题出现在更复杂的情况下。当编译器无法 证明 该条件没有副作用时,它不会优化它,与不使用合同相比,这是一个 runtme 惩罚。
有没有办法表达合约中的条件没有副作用,从而总是优化出来?
所以,这不是答案,而是一些有趣的结果,可能会导向某个地方。
我最终得到了以下玩具代码:
#define contract(x) \
if (![&]() __attribute__((pure, noinline)) { return (x); }()) \
__builtin_unreachable();
bool noSideEffect(int i);
int foo(int i) {
contract(noSideEffect(i));
contract(i == 1);
return i;
}
你也可以 follow along at home;)
noSideEffect
是我们知道没有副作用的函数,但编译器没有。
它是这样的:
GCC
__attribute__((pure))
将函数标记为没有副作用。使用
pure
属性限定noSideEffect
将完全删除函数调用。不错!但是我们不能修改
noSideEffect
的声明。那么如何将它的调用包装在一个本身就是pure
的函数中呢?由于我们正在尝试制作一个独立的宏,lambda 听起来不错。令人惊讶的是,这不起作用...除非我们将
noinline
添加到 lambda!我假设优化器首先内联 lambda,在途中丢失pure
属性,然后再考虑优化对noSideEffect
的调用。使用noinline
,以一种有点违反直觉的方式,优化器能够清除所有内容。太棒了!但是,现在有两个问题:使用
noinline
属性,编译器会为每个 lambda 生成一个主体,即使它们从未被使用过。嗯——链接器可能无论如何都能把它们扔掉。
但更重要的是......我们实际上失去了__builtin_unreachable()
启用的优化:(
总而言之,您可以删除或放回上面代码段中的 noinline
,并最终得到以下结果之一:
和noinline
; Unused code
foo(int)::{lambda()#2}::operator()() const:
mov rax, QWORD PTR [rdi]
cmp DWORD PTR [rax], 1
sete al
ret
foo(int)::{lambda()#1}::operator()() const:
mov rax, QWORD PTR [rdi]
mov edi, DWORD PTR [rax]
jmp noSideEffect(int)
; No function call, but the access to i is performed
foo(int):
mov eax, edi
ret
没有noinline
; No unused code
; Access to i has been optimized out,
; but the call to `noSideEffect` has been kept.
foo(int):
sub rsp, 8
call noSideEffect(int)
mov eax, 1
add rsp, 8
ret
Is there a way to express that the condition in the contract has no side effect, so that it is always optimized out?
不太可能。
众所周知,您不能收集大量断言,将它们转化为假设(通过 __builtin_unreachable
)并期望得到好的结果(例如 John Regehr 的 Assertions Are Pessimistic, Assumptions Are Optimistic)。
一些线索:
CLANG,虽然已经具有
__builtin_unreachable
内在特性,但正是为此目的引入了 __builtin_assume。N4425 - Generalized Dynamic Assumptions(*) 指出:
GCC does not explicitly provide a general assumption facility, but general assumptions can be encoded using a combination of control flow and the
__builtin_unreachable
intrinsic...
The existing implementations that provide generic assumptions use some keyword in the implementationreserved identifier space (
__assume
,__builtin_assume
, etc.). Because the expression argument is not evaluated (side effects are discarded), specifying this in terms of a special library function (e.g.std::assume
) seems difficult.指南支持库(GSL,由 Microsoft 托管,但绝不是 Microsoft 特定的)有 "merely" 此代码:
#ifdef _MSC_VER #define GSL_ASSUME(cond) __assume(cond) #elif defined(__clang__) #define GSL_ASSUME(cond) __builtin_assume(cond) #elif defined(__GNUC__) #define GSL_ASSUME(cond) ((cond) ? static_cast<void>(0) : __builtin_unreachable()) #else #define GSL_ASSUME(cond) static_cast<void>(!!(cond)) #endif
并指出:
// GSL_ASSUME(cond) // // Tell the optimizer that the predicate cond must hold. It is unspecified // whether or not cond is actually evaluated.
*) 文件 rejected:EWG 的指南是在拟议的合同设施内提供功能。
没有办法像死代码一样强制优化代码,因为 GCC 必须始终符合标准。
另一方面,可以使用属性 error
检查表达式是否没有任何副作用,只要无法优化函数调用,它就会显示错误。
一个宏示例,用于检查已优化的内容并执行 UB 传播:
#define _contract(condition) \
{
([&]() __attribute__ ((noinline,error ("contract could not be optimized out"))) {
if (condition) {} // using the condition in if seem to hide `unused` warnings.
}());
if (!(condition))
__builtin_unreachable();
}
error属性在没有优化的情况下不起作用(所以这个宏只能用于release\optimization模式编译)。 请注意,指示合同有副作用的错误会在链接期间显示。
A test that shows an error with unoptimizable contract.
A test that optimizes out a contract but, does UB propagation with it.