为什么默认可构造的类型在某些情况下会提高性能?

sup*_*nun 4 c++ c++20

考虑这三个类:

struct Foo
{
    // causes default ctor to be deleted
    constexpr explicit Foo(int i) noexcept : _i(i) {} 
private:
    int _i;
};

// same as Foo but default ctor is brought back and explicitly defaulted
struct Bar
{
    constexpr Bar() noexcept = default;
    constexpr explicit Bar(int i) noexcept : _i(i) {}

private:
    int _i;
};

// same as Bar but member variable uses brace-or-equals initializer (braces in this case)
struct Baz
{
    constexpr Baz() noexcept = default;
    constexpr explicit Baz(int i) noexcept : _i(i) {}

private:
    int _i{};
};
Run Code Online (Sandbox Code Playgroud)

以下static_asserts 的计算结果为 true (C++20):

static_assert(not std::is_trivially_default_constructible_v<Foo>);
static_assert(std::is_trivially_default_constructible_v<Bar>);
static_assert(not std::is_trivially_default_constructible_v<Baz>);
Run Code Online (Sandbox Code Playgroud)

这意味着只有 Bar 被认为是普通默认可构造的。

我理解如何Foo并且Baz不满足标准定义的条件,但我不明白的是为什么这意味着某些算法可以以Bar它们无法做到的方式优化操作FooBaz

运行时测试示例展示了普通默认可构造的好处: https://quick-bench.com/q/t1W4ItmCoJ60U88_ED9s_7I9Cl0

该测试用 1000 个随机生成的对象填充向量,并测量这样做的运行时间。与int,,,一起跑。我的猜测是矢量重新分配和对象的复制/移动是性能差异体现出来的地方。FooBarBaz

什么是普通默认可构造以实现优化?

为什么编译器(或 std::vector 实现)无法对Foo和应用相同的优化Baz

Bar*_*rry 7

这是 gcc 错过的优化。

基本上,问题是:当vector必须重新分配时,如何将元素从旧存储转移到新存储?gcc 的实现当前尝试执行此操作(为了简洁起见,我删除了一些不相关的代码块):

  // This class may be specialized for specific types.
  // Also known as is_trivially_relocatable.
  template<typename _Tp, typename = void>
    struct __is_bitwise_relocatable
    : is_trivial<_Tp> { };

  template <typename _InputIterator, typename _ForwardIterator,
        typename _Allocator>
    _GLIBCXX20_CONSTEXPR
    inline _ForwardIterator
    __relocate_a_1(_InputIterator __first, _InputIterator __last,
           _ForwardIterator __result, _Allocator& __alloc)
    noexcept(noexcept(std::__relocate_object_a(std::addressof(*__result),
                           std::addressof(*__first),
                           __alloc)))
    {
      _ForwardIterator __cur = __result;
      for (; __first != __last; ++__first, (void)++__cur)
    std::__relocate_object_a(std::__addressof(*__cur),
                 std::__addressof(*__first), __alloc);
      return __cur;
    }

  template <typename _Tp, typename _Up>
    _GLIBCXX20_CONSTEXPR
    inline __enable_if_t<std::__is_bitwise_relocatable<_Tp>::value, _Tp*>
    __relocate_a_1(_Tp* __first, _Tp* __last,
           _Tp* __result,
           [[__maybe_unused__]] allocator<_Up>& __alloc) noexcept
    {
      ptrdiff_t __count = __last - __first;
      if (__count > 0)
    {
      __builtin_memmove(__result, __first, __count * sizeof(_Tp));
    }
      return __result + __count;
    }
Run Code Online (Sandbox Code Playgroud)

这里的第一个重载执行按成员复制,第二个重载执行单个复制memmove- 但前提是类型满足__is_bitwise_relocatable<_Tp>,如您所见,默认为std::is_trivialstd::is_trivial 需要一个简单的默认构造函数,这实际上与此特定优化无关(请参阅#68350,但这就是导致代码路径执行缓慢的逐元素复制而不是单个 memmove 的原因。

您可以通过专门化并查看现在的__is_bitwise_relocatable<Foo>性能来验证情况是否如此。