为海湾合作委员会重现叮叮当当__builtin_assume



最近,我发现了clang的void __builtin_assume(bool),它可以向编译器提供有关程序状态的附加信息。这可以产生巨大的差异,例如:

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

我现在被迫使用GCC,我很好奇是否存在等效的内置程序。不幸的是,我在GCC文档中找不到__builtin_assume。也许存在一个内置程序,但它只是有一个不同的名称?

如果不存在等效的内建,是否有一种方法可以在没有__builtin_assume的情况下产生相同的结果,例如在条件不为true时故意调用未定义的行为?

理想情况下,我想要一个总是可以安全调用的宏,比如:

#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

无论解决方案是什么,它也应该在constexpr函数中工作。

我使用了__builtin_unreachable(),这表明控制流到达这里是未定义的行为。您可以将其封装在if中,从而实质上编写断言。条件可以是false的任何不变量,所以在您的情况下,您可以设置相反的条件。

示例:

// Basically __builtin_assume(count <= 4),
// except that !(count <= 4) is evaluated.
if ( !(count <= 4) ) {
__builtin_unreachable();
}

您可以将其转换为如下断言宏:

// Line break for readability
#define my_assert(...) 
{ if(!(__VA_ARGS__)) __builtin_unreachable(); }

根据问题中的代码,您可以这样使用它:

unsigned sum_small(unsigned data[], size_t count) {
my_assert(count <= 4); // <--- Changed here
unsigned sum = 0;
for (size_t i = 0; i < count; ++i) {
sum += data[i];
}
return sum;
}

我觉得在这里经历未定义的行为是完全不必要的。非常简单的ifabort校验对定义明确,为优化器提供了足够的思考空间:

#include <cstddef>
#include <cstdlib>
// compiles to about 10 instructions at -O3
unsigned sum_small(unsigned data[], size_t count) {
if (count > 4)
std::abort();
unsigned sum = 0;
for (size_t i = 0; i < count; ++i) {
sum += data[i];
}
return sum;
}

无需召唤鼻妖,无需召唤。

由于C++23,这可以使用[[assume]]属性。这就像clang的__builtin_assume一样工作。还有一个__attribute__((__assume__(...))可以在C和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;
}

所有编译器实现[[assume]]可能需要一些时间,但正如您所看到的,有很多后备选项。截至撰写本文时,只有GCC 13对此表示支持。

另请参阅:C++23编译器支持

最新更新