【问题标题】:Performance of std::vector::emplace_back vs. assignment for POD structstd::vector::emplace_back 与 POD 结构分配的性能
【发布时间】:2021-01-28 11:32:23
【问题描述】:

在下面的示例中,对于普通旧数据 (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

【问题讨论】:

  • 我会说一个位置作为两个操作:分配值并增加向量的“虚拟”大小,而分配只是分配
  • 你为什么在测量中忽略了vec.reserve(n);vec.resize(n);?这是两个版本之间的重要区别。之后结果非常相似:ideone.com/G1mXty
  • @mch 如果我在 ideone 网站上使用他的代码,我得到的时间也非常相似(不考虑保留和调整大小),所以它似乎也可能取决于平台
  • 与您的问题无关。如果在类体内定义了成员函数(或构造函数),则无需在它们前面添加inline,因为它们是隐式内联的。
  • 所以我有类似的结果,甚至删除 vec.reserve(n) 并使用 push_back 代替分配! emplace_back 的性能较低!

标签: c++ performance stl


【解决方案1】:

首先,有两个观察:

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

然后是一些高层次的分析:

emplace_back 版本比通过索引访问分配花费的时间更长的原因可能是因为emplace_back 除了创建一个新元素外,还需要将向量增加 1。即使有足够的保留空间,也会增加vector 涉及 (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)。

【讨论】:

  • 非常感谢!这正是我正在寻找的分析 :) 祝你有美好的一天!
【解决方案2】:

您期望测量的内容

emplace_back
vec 的预分配空间中通过emplace_back 创建Date 的实例

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

您可能需要采取的措施(由于优化)

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

作业
只需分配 - 在这种情况下,因为 Date 是一个非常简单的对象 - ixy 的成员在 resize 步骤中已创建的对象。

因此,对于分配案例,您完全错过了创建 Date 对象所需的时间,因为您没有在测量中包含 resize

进一步说明

您的vec.resize(n);assign 案例中默认构造它们时用n 元素填充向量(新放置称为n 次)。在循环中,对这些创建的对象进行赋值。

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

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

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

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

对于像Date 这样的简单类,赋值和放回之间不会有太大区别,因为 assignment 案例循环中的构造通常可以被优化掉.

当您包含 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

【讨论】:

  • 感谢您的回答。正如我在上面的评论中一样,reserve/resize 不是基于我真正感兴趣的更大程序(不是 MWE)故意定时的。发生的操作的轮廓似乎很清楚,但是,我不明白的是,在给定内存的情况下放置 new 并将大小增加一比赋值要昂贵得多。如果优化好,placement new 应该和 assignment 一样昂贵。尺寸计数器的增量是否如此昂贵,还是我错过了其他东西?
  • @LukasKoestler emplace_back 不仅在赋值后增加一个计数器,还在赋值前检查是否有足够的可用空间。如果您resize 一次然后分配每个元素,编译器可以更轻松地优化代码。
  • @LukasKoestler 更新了答案,希望现在更清楚。
猜你喜欢
  • 2021-11-18
  • 1970-01-01
  • 2017-01-12
  • 1970-01-01
  • 2012-06-30
  • 2015-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多