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.
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++.
// 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
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