std::vector::template_back相对于POD结构的赋值的性能



在下面的例子中,我看到了元素赋值(给定正确大小的向量)相对于普通旧数据(POD)结构的emplace_back(给定具有保留存储的向量)的性能优势。有人能详细说明这种差异是从哪里来的吗?

提前非常感谢!

票据

  • 这个问题出现在一个更大的项目中,下面只是MWE
  • 我确实查看了编译器资源管理器,但没有找到一个好的解决方案
  • 我知道赋值之所以有效,是因为结构是POD,但我确实希望编译器能优化开销,因为C++应该有零开销抽象
  • 也欢迎对代码提出任何一般性建议,并感谢您的意见:)

代码

#include <iostream>
#include <vector>
#include <chrono>
#include <numeric>
using std::cout;
using std::endl;
using std::vector;
using std::size_t;
typedef std::chrono::high_resolution_clock hrc;
typedef std::chrono::microseconds ms;
using std::chrono::duration_cast;
struct Data {
int x, y;
inline Data() noexcept: x(0), y(0) {}
inline Data(int x, int y) noexcept: x(x), y(y) {}
};
int main() {
constexpr size_t n = 1000000;
constexpr size_t reps = 5;
for (size_t rep = 0; rep < reps; rep++) {
{
vector<Data> vec;
vec.reserve(n);
auto t1 = hrc::now();
for (size_t i = 0; i < n; i++)
vec.emplace_back(i, -i);
auto t2 = hrc::now();
cout << "Emplace Back: " << duration_cast<ms>(t2 - t1).count() << " ms" << endl;
// Check
size_t sum = 0;
for (auto const &elem : vec)
sum += elem.x;
if (sum != ((n * (n - 1)) / 2))
return EXIT_FAILURE;
}
{
vector<Data> vec;
vec.resize(n);
auto t1 = hrc::now();
for (size_t i = 0; i < n; i++)
vec[i] = Data(i, i);
auto t2 = hrc::now();
cout << "Assign      : " << duration_cast<ms>(t2 - t1).count() << " ms" << endl;
// Check
size_t sum = 0;
for (auto const &elem : vec)
sum += elem.x;
if (sum != ((n * (n - 1)) / 2))
return EXIT_FAILURE;
}
}
}

输出

sysctl -n machdep.cpu.brand_string && clang++ -v && clang++ -o main -std=c++17 -O3 main.cpp && ./main
Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz
Apple clang version 12.0.0 (clang-1200.0.32.29)
Target: x86_64-apple-darwin19.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
Emplace Back: 6162 ms
Assign      : 1000 ms
Emplace Back: 2874 ms
Assign      : 864 ms
Emplace Back: 2149 ms
Assign      : 855 ms
Emplace Back: 2062 ms
Assign      : 934 ms
Emplace Back: 2678 ms
Assign      : 1030 ms

首先,两个观察结果:

  1. 索引访问版本缺少对y参数的否定:
    比较vec.emplace_back(i, -i);vec[i] = Data(i, i);
  2. 打印的时间是微秒,或者说";µs";("ms"通常表示毫秒)
    假设1000000次迭代需要864 us,则一次迭代需要0.8 ns,或者仅需要几个CPU周期。与2.8 ns相比,我们谈论的是每次迭代几个周期的差异

然后进行一些高级分析:

emplace_back版本比通过索引访问进行分配花费更长时间的原因可能是,emplace_back除了创建一个新元素外,还需要将向量增加1。即使有足够的保留空间,向量的增长也涉及(1)如果有足够的空间,则进行检查,以及(2)内部向量大小字段的更新

另一方面,矢量索引访问执行无边界检查,更不用说更新任何大小了。从字面上看,它所做的工作与原始指针取消引用一样多。

元素类型struct Data非常简单。创建、复制或覆盖它所花费的时间应该可以忽略不计。

最后,我们分析生成的程序集,以确定到底发生了什么:

emplace_back版本:

leaq    8000000(%rax), %r14
xorl    %ebp, %ebp
movq    %rbx, %r12
movq    %rax, 8(%rsp)
jmp     .L14
.L73:
movl    %ebp, %eax
movd    %ebp, %xmm0
addq    $1, %rbp
addq    $8, %rbx
negl    %eax
movd    %eax, %xmm5
punpckldq       %xmm5, %xmm0
movq    %xmm0, -8(%rbx)
cmpq    $1000000, %rbp
je      .L72
.L14:
movq    %r14, %r15
subq    %r12, %r15
cmpq    %rbx, %r14
jne     .L73

索引访问版本:

leaq    8000000(%rax), %r13
. . .
pxor    %xmm1, %xmm1
movq    %rax, %r12
movq    %rbp, %rax
.L27:
movdqa  .LC3(%rip), %xmm2
movdqa  %xmm1, %xmm0
addq    $16, %rax
paddq   .LC2(%rip), %xmm1
paddq   %xmm0, %xmm2
shufps  $136, %xmm2, %xmm0
movups  %xmm0, -16(%rax)
cmpq    %rax, %r13
jne     .L27

结论:

  1. 总的来说,编译器在内联和消除两个版本中的对象复制方面都做得很好
  2. 第二个版本是向量化的,每次迭代写入2个元素(可能是由于缺少对y的否定)
  3. 第一个版本正在做更多的工作——我们可以看到额外的计数(addq $1, %rbp)和检查(cmpq $1000000, %rbp)

您期望测量的内容

template_back
通过emplace_backvec中预先分配的空间中创建Date的实例

assignment
创建Date的实例,并将其分配给vec中已存在的Date对象。

您可能应该采取的措施(由于优化)

template_back
同上:由emplace_backvec中预先分配的空间中创建Date的实例
增加size并检查是否必须分配新空间。

赋值
在这种情况下,因为Date是一个非常简单的对象,所以只将i赋值给resize步骤中已经创建的对象的xy的成员。

因此,对于赋值情况,您完全错过了创建Date对象所需的时间,因为测量中没有包括resize

进一步解释

assign情况下的vec.resize(n);通过默认构造n元素来填充向量(新放置称为n次)。在循环中,对那些创建的对象进行赋值。

对于第一种情况(emplace_back),向量的size(而不是容量)在每次迭代中增加一个,并且它以放置新开始每个添加对象的生命周期,而对于第二种情况(分配),size不会更改,您只将Date对象分配给已构建的对象。

编译器可以优化分配情况,只将xy分配给向量中已经存在的实例,而不创建中间的Date对象。

要以有意义的方式进行测量,您需要包括vec.reserve(n)vec.resize(n)。目前,您只是测量编译器优化过程中的一些差异。

如果还包括vec.reserve(n)vec.resize(n),则度量值会更接近。

对于像Date这样的简单类,赋值和模板返回之间不会有太大区别,因为赋值情况的循环中的构造通常可以被优化掉。

当您输入vec.reserve(n)vec.resize(n)时,时间为:

gcc 8.4

Emplace Back: 11594 ms
Assign      : 11516 ms
Emplace Back: 2001 ms
Assign      : 2691 ms
Emplace Back: 2523 ms
Assign      : 1847 ms
Emplace Back: 1956 ms
Assign      : 1277 ms
Emplace Back: 949 ms
Assign      : 903 ms

叮当声

Emplace Back: 2115 ms
Assign      : 2640 ms
Emplace Back: 765 ms
Assign      : 766 ms
Emplace Back: 666 ms
Assign      : 540 ms
Emplace Back: 535 ms
Assign      : 515 ms
Emplace Back: 537 ms
Assign      : 543 ms

最新更新