如何引导基于Assert的GCC优化而不增加运行时成本?

k10s72fa  于 2023-08-06  发布在  其他
关注(0)|答案(4)|浏览(100)

我有一个宏在我的代码中使用,在调试模式下:

#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


条件,以及依赖于它的一切,都已经优化了。
我的问题出现在更复杂的情况下。当编译器不能 * 证明 * 条件没有副作用时,它不会优化它,与不使用合约相比,这是一种运行时损失。
有没有一种方法可以表示合同中的条件没有副作用,因此它总是被优化出来?

vptzau2j

vptzau2j1#

有没有一种方法可以表达合同中的条件没有副作用,这样总是优化出来的?
不太可能
众所周知,你不能把一个大的Assert集合,把它们变成假设(通过__builtin_unreachable),并期望好的结果(例如Assertions Are Pessimistic, Assumptions Are Optimistic by John Regehr)。
一些线索:

GCC没有显式地提供一般假设工具,但是一般假设可以使用控制流和__builtin_unreachable内在
...
提供一般假设的现有实现在实现中使用一些关键字保留的标识符空间(__assume__builtin_assume等)。因为表达式参数不被计算(副作用被丢弃),所以根据特殊的库函数(例如std::assume)似乎很难。

  • 指南支持库(GSL,由Microsoft托管,但不是Microsoft特定的)“仅仅”有以下代码:
#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.

  • )论文被拒绝:EWG的指导是在拟议的合同设施中提供功能。
pftdvrlh

pftdvrlh2#

所以,不是一个答案,而是一些有趣的结果,可能会有什么结果。
最后我得到了下面的代码:

#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是一个我们知道没有副作用的函数,但编译器没有。
事情是这样的:

  1. GCC使用__attribute__((pure))将函数标记为无副作用。
    1.使用pure属性限定noSideEffect将完全删除函数调用。好极了!
    1.但我们不能修改noSideEffect的声明。那么,把它的调用 Package 在一个本身是pure的函数中怎么样?既然我们要创建一个自包含的宏,那么lambda听起来不错。
    1.令人惊讶的是,这不起作用......除非我们在λ上加上noinline!我假设优化器首先内联lambda,在考虑优化对noSideEffect的调用之前,会在途中丢失pure属性。对于noinline,优化器能够以一种有点违反直觉的方式清除所有内容。好极了!
    1.不过,现在有两个问题:使用noinline属性,编译器会为每个lambda生成一个主体,即使它们从未被使用过。Meh --链接器可能无论如何都能把它们扔掉。
    但更重要的是......我们实际上丢失了__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

yks3o0rb

yks3o0rb3#

没有办法强制优化代码,就好像它是死代码一样,因为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.

nxowjjhe

nxowjjhe4#

从C++23开始,这可以使用[[assume]] attribute。如果给定条件的计算结果不是true,则此属性会使其成为未定义行为:举例来说:

// define constract(...) conditionally
#if !defined(NDEBUG)
  #define contract(...) ((__VA_ARGS__) ? void() : \
                                         throw ::std::logic_error("contract violated"))

// feature testing macro to avoid warnings for unknown attributes
#elif __has_cpp_attribute(assume) >= 202207L
  #define contract(...) [[assume(__VA_ARGS__)]] // <<< C++23 ASSUME HERE

// clang has a similar feature as a compiler intrinsic
#elif defined(__clang__)
  #define contract(...) __builtin_assume(__VA_ARGS__)

// fallback case, might want to add more detections
#else
  #define contract(...) ((__VA_ARGS__) ? void() : __builtin_unreachable())
#endif

字符串
然后我们可以像往常一样使用contract

int foo(int i) {
    contract(i == 1);
    return i;
}


这编译为:

foo(int):
        mov     eax, 1
        ret


所有编译器实现这一点可能需要一些时间。在撰写本文时,只有GCC 13支持此功能。还要记住,编译器使用假设的能力是有限的。它不能从任意复杂的表达式中推断出信息,所以使用尽可能简单的表达式。

相关问题