c++ 为什么在某些情况下,一个普通的默认可构造类型会提高性能?

sdnqo3pr  于 2023-04-13  发布在  其他
关注(0)|答案(2)|浏览(163)

考虑这三个类:

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

下面的static_assert s计算为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>);

这意味着只有Bar被认为是普通默认可构造的。
我理解FooBaz为什么不满足标准定义的条件,但我不明白的是,为什么这意味着某些算法可以优化Bar上的操作,而在FooBaz上却不能。
运行时测试的示例展示了普通默认可构造的好处:https://quick-bench.com/q/t1W4ItmCoJ60U88_ED9s_7I9Cl0
测试用1000个随机生成的对象填充一个向量,并测量这样做的运行时间。运行intFooBarBaz。我猜是向量重新分配和对象的复制/移动是性能差异的体现。

平凡默认可构造是什么让优化成为可能?
为什么编译器(或std::vector实现)无法在FooBaz上应用相同的优化?

lhcgjxsq

lhcgjxsq1#

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

这里的第一个重载执行成员方式的复制,第二个重载执行单个memmove-但仅当类型满足__is_bitwise_relocatable<_Tp>时,如您所见,默认值为std::is_trivial。但这就是导致代码路径进行缓慢的元素复制而不是单个memmove的原因。
您可以通过专门化__is_bitwise_relocatable<Foo>并查看性能now lines up来验证这一点。

8gsdolmq

8gsdolmq2#

我理解Foo和Baz为什么不满足标准定义的条件,但我不明白的是,为什么这意味着某些算法可以优化Bar上的操作,而在Foo或Baz上却不能。
这不是真实的的实现,而只是一个概念上的暗示:

Bar* default_alloc_x_Bars(int x)
{
    return (Bar*)(char*)malloc(sizeof(Bar)*x); //returns nullptr if allocation unsuccesful
}

Baz* default_alloc_x_Bazs(int x)
{
    char* buffer = (char*)malloc(sizeof(Baz)*x);

    if(buffer)
        memset((void*)buffer,0,sizeof(Baz)*x);

    return (Baz*)buffer; //returns nullptr if allocation unsuccesful
}

_i未初始化的情况下,不可能创建法律的的BazFoo,因此您不能简单地获取一块内存并声明它是Baz/Foo
Bar不会初始化_i,因此任何给定的大小和对齐方式正确的内存块在功能上都是Bar
(这是一个复杂的问题,你不能像我们写普通C一样简单地转换内存,但是作为一个概念模型,为什么琐碎的构造很重要,这几乎是事情的全部)

相关问题