c++gccclangbuilt-inassumption

Reproducing clang's __builtin_assume for GCC


Recently, I discovered void __builtin_assume(bool) for clang, which can provide additional information about the state of the program to the compiler. This can make a huge difference, like for example:

#include <cstddef>

// compiles to about 80 instructions at -O3
unsigned sum(unsigned data[], size_t count) {
    unsigned sum = 0;
    for (size_t i = 0; i < count; ++i) {
        sum += data[i];
    }
    return sum;
}

// compiles to about 10 instructions at -O3
unsigned sum_small(unsigned data[], size_t count) {
    __builtin_assume(count <= 4);
    unsigned sum = 0;
    for (size_t i = 0; i < count; ++i) {
        sum += data[i];
    }
    return sum;
}

I am forced to use GCC at this time and I am curious whether there exists an equivalent builtin. Unfortunately I could not find __builtin_assume in the GCC documentation. Maybe there exists a builtin but it just has a different name?

If there doesn't exist an equivalent builtin, is there maybe a way to produce the same result without __builtin_assume, such as intentionally invoking undefined behavior when the condition is not true?

Ideally, I would like a macro which is always safe to call like:

#if ... // detect clang
#define MY_ASSUME(condition) __builtin_assume(condition)
#elif ... // detect GCC
#define MY_ASSUME(condition) __gcc_builtin_assume_equivalent(condition)
#else
#define MY_ASSUME(condition)
#endif

Whatever the solution is, it should also work in a constexpr function.


Solution

  • Since C++23, this is possible using the [[assume]] attribute. This works just like clang's __builtin_assume. There is also an __attribute__((__assume__(...)) that works in C and C++.

    Definition of Assumption Macro

    // define an ASSUME(...) function-style macro so we only need to detect compilers
    // in one place
    
    // Comment this out if you don't want assumptions to possibly evaluate.
    // This may happen for implementations based on unreachable() functions.
    #define DANGEROUS_BEHAVIOR_ASSUMPTIONS_ALLOWED_TO_EVALUATE 1
    
    // preferred option: C++ standard attribute
    #ifdef __has_cpp_attribute
      #if __has_cpp_attribute(assume) >= 202207L
        #define ASSUME(...) [[assume(__VA_ARGS__)]]
      #endif
    #endif
    // first fallback: compiler intrinsics/attributes for assumptions
    #ifndef ASSUME
      #if defined(__clang__)
        #define ASSUME(...) do { __builtin_assume(__VA_ARGS__); } while(0)
      #elif defined(_MSC_VER)
        #define ASSUME(...) do { __assume(__VA_ARGS__); } while(0)
      #elif defined(__GNUC__)
        #if __GNUC__ >= 13
          #define ASSUME(...) __attribute__((__assume__(__VA_ARGS__)))
        #endif
      #endif
    #endif
    // second fallback: possibly evaluating uses of unreachable()
    #if !defined(ASSUME) && defined(DANGEROUS_BEHAVIOR_ASSUMPTIONS_ALLOWED_TO_EVALUATE)
      #if defined(__GNUC__)
        #define ASSUME(...) do { if (!bool(__VA_ARGS__)) __builtin_unreachable(); } while(0)
      #elif __cpp_lib_unreachable >= 202202L
        #include <utility>
        #define ASSUME(...) do { if (!bool(__VA_ARGS__)) ::std::unreachable(); ) while(0)
      #endif
    #endif
    // last fallback: define macro as doing nothing
    #ifndef ASSUME
      #define ASSUME(...)
    #endif
    

    Usage Example

    unsigned sum_small(unsigned data[], size_t count) {
        ASSUME(count <= 4);
    
        unsigned sum = 0;
        for (size_t i = 0; i < count; ++i) {
            sum += data[i];
        }
        return sum;
    }
    

    It may take some time for all compilers to implement [[assume]], but as you can see, there are plenty fallback options. As of the time of writing, only GCC 13 supports this.

    See also: C++23 Compiler Support