将__builtin_expect降级为内联函数是否安全?

时间:2015-12-18 20:42:39

标签: c++ built-in gcc-extensions

我正在研究一些定义

的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 ...它是否可以在内联辅助函数内部工作时有所不同?

2 个答案:

答案 0 :(得分:4)

使用经过尝试和信任的宏,即使我们都知道一般要避免使用宏。 inline功能根本不起作用。或者 - 特别是如果您使用GCC - 完全忘记__builtin_expect并使用配置文件引导优化(PGO)代替实际的分析数据。

__builtin_expect非常特别,因为它实际上并没有“做”任何事情,只是暗示了编译器最有可能采取的分支。如果在不是分支条件的上下文中使用内置函数,则编译器必须将此信息与值一起传播。直观地说,我原本预计会发生这种情况。有趣的是,GCCClang的文档对此并不十分明确。但是,我的实验表明,Clang显然没有传播这些信息。至于海湾合作委员会,我仍然需要找到一个实际上关注内置的程序,所以我无法确定。 (换句话说,无论如何,它都无关紧要。)

我测试了以下功能。

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

make_jitter只是return范围内的随机整数[ - m , m ]其中 m 是它的第一个论点。

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的代码是精心设计的,任何一个分支都具有可比的成本,在没有达到预期的情况下会给成本略高一些。还确保编译器不会生成一个矢量化循环,其分支将是无关紧要的。

对于基准测试,来自范围[0,numbers]和随机INT_MAX的100000000个随机整数的向量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);
}

虽然在概念上有三种不同的版本,但我比较了1 st 与2 nd 和1 st 与3 rd 。因此,1 st 的数据基本上被收集两次。 2 nd 和3 rd 被称为“暗示” 在情节中。

以下图表的横轴表示LIKELY注释的命中率,纵轴表示循环每次迭代的平均CPU时间。

以下是1 st 与2 nd 的关系曲线。

enter image description here

正如您所看到的,GCC实际上忽略了提示,无论是否给出提示,都会产生同等效果的代码。另一方面,Clang显然关注这个提示。如果命中率降低(即提示错误),则代码会受到惩罚,但是对于高命中率(即提示是好的),代码优于GCC生成的代码。

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

相比之下,这是1 st 与3 rd 的关系图。

enter image description here

两个编译器都生成基本上相同的代码。对于海湾合作委员会来说,这并没有多大说明,但就Clang而言,__builtin_expect似乎在包含在一个函数中时会被考虑在内,这使得它在所有命中时都会对GCC松散-rates。

因此,总而言之,不要将函数用作包装器。如果宏写得正确,则没有危险。 (除了污染名称空间。)__builtin_expect已经表现出来(至少就其参数的评估而言)就像一个函数。在宏中包装函数调用对其参数的评估没有任何惊人的影响。

我意识到这不是你的问题所以我会保持简短,但总的来说,更喜欢收集实际的分析数据,而不是手工猜测可能的分支。数据将更加准确,GCC将更加关注它。

答案 1 :(得分:1)

无法保证编译器内联函数。大多数现代编译器仅将var rewriteModule = require('http-rewrite-middleware'); var rewriteMiddleware = rewriteModule.getMiddleware([ {from: '^/service/css/(.*)$', to: '/css/_selfservice/$1'} ]); 关键字视为提示。如果您强制使用inline使用GCC(或__attribute__((always_inline))使用MSVC)进行内联,那么使用内联函数还是宏(甚至是__forceinline {{3 }})。否则,该功能可能无法内联。例如,关闭优化的GCC may not work。在这种情况下,生成的代码会相当慢。我坚持使用宏来保证安全。