繁体   English   中英

将__builtin_expect委托给内联函数是否安全?

[英]is it safe to relegate __builtin_expect to an inline function?

我正在研究一些定义的C ++代码

#define LIKELY(x)   (__builtin_expect((x), 1))

我想知道-为什么不使用内联函数? 即为什么不

template <typename T> inline 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非常特别,因为它实际上并没有“做任何事情”,而只是暗示编译器将最有可能采用哪个分支。 如果您在非分支条件的上下文中使用内置函数,则编译器将不得不将此信息与值一起传播。 凭直觉,我原本希望发生这种情况。 有趣的是, GCCClang的文档对此并不十分明确。 但是,我的实验表明,Clang显然没有传播此信息。 至于GCC,我仍然必须找到一个真正关注内置程序的程序,因此我无法确定。 (或者换句话说,没关系。)

我已经测试了以下功能。

std::size_t
do_computation(std::vector<int>& 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;
}

, ] where is its first argument. make_jitter只是return范围为[− ]的随机整数,其中是其第一个参数。

int
make_jitter(const int margin, std::mt19937& rndeng)
{
  auto rnddist = std::uniform_int_distribution<int> {-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的代码进行了精心设计,以使每个分支的成本do_computation比较,从而使未达到期望的情况的成本略高。 还确保了编译器不会生成对其分支无关紧要的矢量化循环。

为基准,载体numbers从区间[0,100个000 000随机整数INT_MAX ]和随机base_threshold形成区间[0, INT_MAX - margin ](带有margin设置为100)与非确定性地接种产生伪随机数生成器。 do_computation(numbers, base_threshold, margin, …) (在单独的转换单元中编译)被调用了四次,并测量了每次运行的执行时间。 第一次运行的结果被丢弃,以消除冷缓存效应。 相对于命中率( LIKELY批注正确的相对频率)绘制其余运行的平均和标准偏差。 添加了“抖动”以使这四个运行的结果不同(否则,我会担心编译器太聪明),同时仍然保持命中率基本固定。 通过这种方式收集了100个数据点。

我已经用GCC 5.3.0和Clang 3.7.0编译了程序的三个不同版本,并向它们传递了-DNDEBUG-O3-std=c++14标志。 版本仅在定义LIKELY的方式上有所不同。

// 1st version
#define LIKELY(X) static_cast<bool>(X)

// 2nd version
#define LIKELY(X) __builtin_expect(static_cast<bool>(X), true)

// 3rd version
inline bool
LIKELY(const bool x) noexcept
{
  return __builtin_expect(x, true);
}

尽管从概念上讲,这是三个不同的版本,但我比较了1st2nd以及1st3rd 因此,第一次的数据基本上被收集了两次。 2 3 被称为在图解为“微调后的”。

下图的水平轴显示了LIKELY批注的命中率,垂直轴显示了每次循环的平均CPU时间。

这是1st2nd的曲线图。

在此处输入图片说明

如您所见,无论是否提供了提示,GCC都会有效地忽略提示,从而产生性能均等的代码。 另一方面,Clang显然要注意提示。 如果命中率下降得很低(即,提示是错误的),则该代码将受到惩罚,但对于命中率较高(即,提示是良好的),该代码将优于GCC生成的代码。

如果您想知道曲线的山形性质,那就是工作中的硬件分支预测器! 它与编译器无关。 还要注意,这种效果如何使__builtin_expect效果完全__builtin_expect which,这可能是不必太担心的原因。

相反,这是1st3rd的关系图

在此处输入图片说明

两种编译器都产生本质上等同的代码。 对于GCC,这并没有多说,但就Clang而言,当将__builtin_expect包裹在一个函数中时,似乎没有考虑到__builtin_expect ,这对于所有命中率而言,它对于GCC而言都是松散的。

因此,总而言之,不要将函数用作包装器。 如果正确编写了宏,则没有危险。 (除了污染名称空间。) __builtin_expect行为(至少就其参数的评估而言)类似于函数。 在宏中包装函数调用对其参数的求值没有令人惊讶的影响。

我意识到这不是您的问题,因此我将其简化,但总的来说,宁愿收集实际的分析数据,也不愿手动猜测可能的分支。 数据将更加准确,GCC将更加关注它。

不能保证编译器可以内联函数。 大多数现代编译器仅将inline关键字视为提示。 如果您在GCC上强制使用__attribute__((always_inline))进行内联(对于MSVC则使用__forceinline ),则无论使用内联函数还是使用宏都没关系(但即使__forceinline 也可能不起作用 )。 否则,该函数可能不会被内联。 例如,GCC 不会在关闭优化的情况下内联函数 在这种情况下,生成的代码将相当慢。 为了安全起见,我会坚持使用宏。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM