我正在研究一些定义的C ++代码
#define LIKELY(x) (__builtin_expect((x), 1))
我想知道-为什么不使用内联函数?即为什么不
templateinline T likely(T x) { return __builtin_expect((x), 1); }
(或许
inline int likely(int x) { return __builtin_expect((x), 1); }
因为x应该是某些条件检查的结果)
宏和函数应该基本相同,对吗?但是后来我想知道:也许是因为__builtin_expect
...在内联辅助函数中工作的不同吗?
即使我们都知道通常应避免使用宏,也应使用经过尝试和受信任的宏。这些inline
功能根本不起作用。另外,尤其是在使用GCC的情况下,__builtin_expect
完全不要使用实际配置数据中的配置文件引导的优化(PGO)。
的__builtin_expect
是,它并没有真正“做”什么很特别的,而只是暗示朝哪个分支将最有可能采取的编译器。如果您在非分支条件的上下文中使用内置函数,则编译器将不得不将此信息与值一起传播。凭直觉,我原本希望发生这种情况。有趣的是,GCC和Clang的文档对此并不十分明确。但是,我的实验表明,Clang显然没有传播此信息。至于GCC,我仍然必须找到一个真正关注内置程序的程序,因此我无法确定。(或者换句话说,没关系。)
我已经测试了以下功能。
std::size_t
do_computation(std::vector& numbers,
const int base_threshold,
const int margin,
std::mt19937& rndeng,
std::size_t *const hitsptr)
{
assert(base_threshold >= margin && base_threshold <= INT_MAX - margin);
assert(margin > 0);
benchmark::clobber_memory(numbers.data());
const auto jitter = make_jitter(margin - 1, rndeng);
const auto threshold = base_threshold + jitter;
auto count = std::size_t {};
for (auto& x : numbers)
{
if (LIKELY(x > threshold))
{
++count;
}
else
{
x += (1 - (x & 2));
}
}
benchmark::clobber_memory(numbers.data());
// My benchmarking framework swallows the return value so this trick with
// the pointer was needed to get out the result. It should have no effect
// on the measurement.
if (hitsptr != nullptr)
*hitsptr += count;
return count;
}
make_jitter
只是return
一个在[ -m,m ] 范围内的随机整数,其中m是其第一个参数。
int
make_jitter(const int margin, std::mt19937& rndeng)
{
auto rnddist = std::uniform_int_distribution {-margin, margin};
return rnddist(rndeng);
}
benchmark::clobber_memory
是一个禁止操作,它拒绝编译器优化矢量数据的修改。这样实现。
inline void
clobber_memory(void *const p) noexcept
{
asm volatile ("" : : "rm"(p) : "memory");
}
的声明用do_computation
注释__attribute__ ((hot))
。原来,这影响了编译器大量应用的优化。
do_computation
精心设计的代码使每个分支机构都具有可比较的成本,从而给未达到期望的情况带来了更多的成本。还确保了编译器不会生成矢量化循环,对于该循环而言,分支将无关紧要。
对于基准测试,使用不确定的种子伪随机数生成器生成了一个numbers
范围为[0,INT_MAX
]的100000000个随机整数和base_threshold
间隔为[0,INT_MAX
- margin
](margin
设置为100)的随机数的向量。do_computation(numbers, base_threshold, margin, …)
(在单独的翻译单元中编译)(调用了四次),并测量了每次运行的执行时间。第一次运行的结果被丢弃,以消除冷缓存效应。将剩余运行的平均和标准偏差与命中率(相对频率LIKELY
注释正确)。添加了“抖动”以使这四个运行的结果不相同(否则,我会担心编译器太聪明),同时仍然保持命中率基本固定。通过这种方式收集了100个数据点。
我编了三个不同版本的程序既GCC 5.3.0和3.7.0锵通过他们-DNDEBUG
,-O3
和-std=c++14
标志。版本仅在LIKELY
定义方式上有所不同。
// 1st version
#define LIKELY(X) static_cast(X)
// 2nd version
#define LIKELY(X) __builtin_expect(static_cast(X), true)
// 3rd version
inline bool
LIKELY(const bool x) noexcept
{
return __builtin_expect(x, true);
}
尽管从概念上讲,这是三个不同的版本,但我比较了1st和2nd以及1st和3rd。因此,第一次的数据基本上被收集了两次。2 次和3 次被称为在图解为“微调后的”。
下图的LIKELY
横轴显示注释的命中率,纵轴显示循环每次迭代的平均CPU时间。
这是1st与2nd的曲线图。
如您所见,无论是否提供了提示,GCC都会有效地忽略提示,从而产生性能均等的代码。另一方面,Clang显然要注意提示。如果命中率下降得很低(即,提示是错误的),则该代码将受到惩罚,但对于命中率较高(即,提示是良好的),该代码将优于GCC生成的代码。
如果您想知道曲线的山形性质,那就是工作中的硬件分支预测器!它与编译器无关。另请注意,这种效果如何使的效果完全相形见__builtin_expect
which,这可能是不必过多担心的原因。
相反,这是1st与3rd的关系图。
两种编译器都产生本质上等同的代码。对于GCC而言,这并不多说,但是就Clang而言,__builtin_expect
当包裹在一个函数中时,似乎并没有考虑到这一点,因为对于所有命中率而言,该函数使其相对于GCC而言松散。
因此,总而言之,不要将函数用作包装器。如果正确编写了宏,则没有危险。(除了污染名称空间。)__builtin_expect
已经像函数一样(至少就其参数的评估而言)。在宏中包装函数调用对其参数的求值没有令人惊讶的影响。
我意识到这不是您的问题,因此我将其简化,但总的来说,宁愿收集实际的分析数据,也不愿手动猜测可能的分支。数据将更加准确,GCC将更加关注它。