为什么在循环之前将 const 标量值分配给 const 会有所帮助?

Why does it help to assign a const& scalar value to a const before a loop?

在 GCC 5.4.0 中 stl_algobase.h 我们有:

  template<typename _ForwardIterator, typename _Tp>
    inline typename
    __gnu_cxx::__enable_if<!__is_scalar<_Tp>::__value, void>::__type
    __fill_a(_ForwardIterator __first, _ForwardIterator __last,
         const _Tp& __value)
    {
      for (; __first != __last; ++__first)
    *__first = __value;
    }

  template<typename _ForwardIterator, typename _Tp>
    inline typename
    __gnu_cxx::__enable_if<__is_scalar<_Tp>::__value, void>::__type
    __fill_a(_ForwardIterator __first, _ForwardIterator __last,
         const _Tp& __value)
    {
      const _Tp __tmp = __value;
      for (; __first != __last; ++__first)
    *__first = __tmp;
    }

我不明白为什么标量变体比一般变体有任何优势。我的意思是,它们不会被编译成完全相同的东西吗?将 __value 从堆栈加载到寄存器并在整个循环中使用该寄存器?

这起源于 2004 年的 SVN 修订版 83645(git 提交 8ba26e53),当时两个 __fill_a 变体都作为辅助结构实现:

template<typename>
struct __fill
{
  template<typename _ForwardIterator, typename _Tp>
    static void
    fill(_ForwardIterator __first, _ForwardIterator __last,
     const _Tp& __value)
    {
  for (; __first != __last; ++__first)
    *__first = __value;
}
};

template<>
struct __fill<__true_type>
{
  template<typename _ForwardIterator, typename _Tp>
    static void
    fill(_ForwardIterator __first, _ForwardIterator __last,
     const _Tp& __value)
    {
  const _Tp __tmp = __value;
  for (; __first != __last; ++__first)
    *__first = __tmp;
}
};

关于此主题的文档很少,但 Dan Nicolaescu 和 Paolo Carlini 的原始提交在提交消息中包含提示:

  • include/bits/stl_algobase.h (__fill, __fill_n): New helpers for fill and fill_n, respectively: when copying is cheap, use a temporary to avoid a memory read in each iteration.

鉴于那些 are/were 标准库的维护者,我认为他们知道他们在做什么:他们解决了 引用通常作为指针实现的问题 。毕竟,它们只是一个已经存在的内存位置的新别名。这就是最初有两种变体的原因。请注意 __true_type 是在 fill 调用中决定的:

  typedef typename __type_traits<_Tp>::has_trivial_copy_constructor
    _Trivial;
  std::__fill<_Trivial>::fill(__first, __last, __value);

使用 std::enable_if,或者更确切地说是它的 GCC 变体,Carlini 删除了那些助手,并用您已经提供的版本替换了它们。逻辑仍然成立:对于标量类型,您希望具有一些本地值。如果您的范围位于另一个内存区域而不是您的值并且跨越多个页面并溢出您的 L1 缓存,您不希望为该值锁定缓存的一部分。这对于局部变量来说是微不足道的。

然而,语义很重要。 std::fill 正好生成 std::distance(first, last) 个副本。使用标量值,我们知道额外的副本不会有副作用。使用用户定义的类型?好吧,我们不知道。这就是为什么在第一种情况下不能使用 const auto tmp = __value; 变体。

这就是为什么你最终得到两个,实际上是三个变体的原因:

  • 一个用于标量值,您可以保留该值 "close" 并帮助优化器
  • 一个用于类似字节的值,您可以在其中使用 memset
  • 一个用于所有其他类型,您不能在其中干涉语义。