How to guide GCC optimizations based on assertions without runtime cost?

后端 未结 3 1490
独厮守ぢ
独厮守ぢ 2020-12-13 18:57

I have a macro used all over my code that in debug mode does:

#define contract(condition) \\
    if (!(condition)) \\
        throw exception(\"a contract ha         


        
相关标签:
3条回答
  • 2020-12-13 19:36

    Is there a way to express that the condition in the contract has no side effect, so that it is always optimized out?

    Not likely.

    It's known that you cannot take a big collection of assertions, turn them into assumptions (via __builtin_unreachable) and expect good results (e.g. Assertions Are Pessimistic, Assumptions Are Optimistic by John Regehr).

    Some clues:

    • CLANG, while already having the __builtin_unreachable intrinsic, introduced __builtin_assume exactly for this purpose.

    • N4425 - Generalized Dynamic Assumptions(*) notes that:

      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 implementation­reserved 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.

    • The Guidelines Support Library (GSL, hosted by Microsoft, but in no way Microsoft specific) has "merely" this code:

      #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
      

      and notes that:

      // GSL_ASSUME(cond)
      //
      // Tell the optimizer that the predicate cond must hold. It is unspecified
      // whether or not cond is actually evaluated.
      

    *) Paper rejected: EWG's guidance was to provide the functionality within the proposed contract facilities.

    0 讨论(0)
  • 2020-12-13 19:44

    There is no way to force optimize out code as-if it was dead-code, because GCC has to always be complaint with the standard.

    On the other hand the expression can be checked to not have any side effects by using the attribute error which will show an error whenever a function's call could not be optimized out.

    An example of a macro that checks whatever is optimized out and does UB propagation:

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

    The error attribute does not work without optimization (so this macro can only be used for release\optimization mode compilation). Note that the error that indicates whatever the contract has side effects is shown during linkage.

    A test that shows an error with unoptimizable contract.

    A test that optimizes out a contract but, does UB propagation with it.

    0 讨论(0)
  • 2020-12-13 19:45

    So, not an answer, but some interesting results which might lead somewhere.

    I ended up with the following toy code:

    #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;
    }
    

    You can follow along at home, too ;)

    noSideEffect is the function which we know has no side effects, but the compiler doesn't.
    It went like this:

    1. GCC has __attribute__((pure)) to mark a function as having no side effect.

    2. Qualifying noSideEffect with the pure attribute removes the function call completely. Nice!

    3. But we cannot modify the declaration of noSideEffect. So how about wrapping its call inside a function that is itself pure? And since we are trying to make a self-contained macro, a lambda sounds good.

    4. Surprisingly, that doesn't work... unless we add noinline to the lambda! I suppose that the optimizer inlines the lambda first, losing the pure attribute on the way, before considering optimizing out the call to noSideEffect. With noinline, in a somewhat counter-intuitive way, the optimizer is able to wipe everything out. Great!

    5. However, there are two issues now: with the noinline attribute, the compiler generates a body for each lambda, even if they are never used. Meh -- the linker will probably be able to throw them away anyway.
      But more importantly... We actually lost the optimizations that __builtin_unreachable() had enabled :(

    To sum it up, you can remove or put back noinline in the snippet above, and end up with one of these results:

    With 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
    

    Without 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
    
    0 讨论(0)
提交回复
热议问题