考虑这三个类:
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被认为是普通默认可构造的。
我理解Foo
和Baz
为什么不满足标准定义的条件,但我不明白的是,为什么这意味着某些算法可以优化Bar
上的操作,而在Foo
或Baz
上却不能。
运行时测试的示例展示了普通默认可构造的好处:https://quick-bench.com/q/t1W4ItmCoJ60U88_ED9s_7I9Cl0
测试用1000个随机生成的对象填充一个向量,并测量这样做的运行时间。运行int
,Foo
,Bar
,Baz
。我猜是向量重新分配和对象的复制/移动是性能差异的体现。
平凡默认可构造是什么让优化成为可能?
为什么编译器(或std::vector实现)无法在Foo
和Baz
上应用相同的优化?
2条答案
按热度按时间lhcgjxsq1#
这是gcc的一个遗漏的优化。
基本上,问题是:当
vector
必须重新分配时,如何将元素从旧存储转移到新存储?gcc的实现目前正在尝试这样做(为了简洁起见,我删除了一些不相关的代码块):这里的第一个重载执行成员方式的复制,第二个重载执行单个
memmove
-但仅当类型满足__is_bitwise_relocatable<_Tp>
时,如您所见,默认值为std::is_trivial
。但这就是导致代码路径进行缓慢的元素复制而不是单个memmove的原因。您可以通过专门化
__is_bitwise_relocatable<Foo>
并查看性能now lines up来验证这一点。8gsdolmq2#
我理解Foo和Baz为什么不满足标准定义的条件,但我不明白的是,为什么这意味着某些算法可以优化Bar上的操作,而在Foo或Baz上却不能。
这不是真实的的实现,而只是一个概念上的暗示:
在
_i
未初始化的情况下,不可能创建法律的的Baz
或Foo
,因此您不能简单地获取一块内存并声明它是Baz
/Foo
。Bar
不会初始化_i
,因此任何给定的大小和对齐方式正确的内存块在功能上都是Bar
。(这是一个复杂的问题,你不能像我们写普通C一样简单地转换内存,但是作为一个概念模型,为什么琐碎的构造很重要,这几乎是事情的全部)