【问题标题】:Insert or push_back to end of a std::vector?插入或 push_back 到 std::vector 的末尾?
【发布时间】:2019-12-22 14:00:16
【问题描述】:

以下两种在std::vector末尾插入新元素的方法在性能上是否存在差异:

方法一

std::vector<int> vec = { 1 };
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);

方法二

std::vector<int> vec = { 1 };
int arr[] = { 2,3,4,5 };
vec.insert(std::end(vec), std::begin(arr), std::end(arr));

就个人而言,我喜欢方法2,因为它简洁明了,可以一次性插入数组中的所有新元素。但是

  • 性能有什么不同吗?
  • 毕竟,他们做同样的事情。不是吗?

更新

首先我没有用所有元素初始化向量的原因是,在我的程序中,我根据条件添加了剩余的元素。

【问题讨论】:

  • 还有另一种选择:std::vector&lt;int&gt; vec { 1,2,3,4,5 };
  • 创建一个小型测试程序,每个测试程序执行一百万次。在启用优化的情况下进行构建,并进行测试和测量。
  • 哦,虽然最终结果(一个包含五个元素的向量)这两种方法做不同的事情。最重要的区别是向量中元素所需数据的分配和重新分配。第一个变体可能导致四次重新分配,并且容量和大小可能不同,而第二个变体可能只有一次重新分配,并且容量和大小可能相同。
  • 我个人喜欢方法2,因为它简洁明了,可以一次性插入数组中的所有新元素。这是重要的原因,性能是次要的。

标签: c++ performance c++11 stl insert


【解决方案1】:

如果向量需要重新分配,这两种方法可能会有所不同。

您的第二种方法,使用迭代器范围调用一次insert() 成员函数:

vec.insert(std::end(vec), std::begin(arr), std::end(arr));

由于insert() 正在获得随机访问迭代器,因此将能够提供一次性插入元素所需的所有内存分配的优化,即,需要恒定时间才能知道范围的大小,因此可以在复制元素之前完成整个内存分配,并且不会在调用期间进行重新分配。

您的第一种方法,即对push_back() 成员函数的单独调用,可能会触发多次重新分配,具体取决于要插入的元素数量和最初为向量保留的内存。

请注意,上述优化可能不适用于 forward 或 双向迭代器,因为在范围大小上需要线性时间才能知道元素的数量要插入。但是,多个内存分配所需的时间可能会使计算这些情况的范围长度所需的时间相形见绌,因此他们可能仍然实施这种优化。对于输入迭代器,这种优化甚至是不可能的,因为它们是单通道迭代器。

【讨论】:

    【解决方案2】:

    push_back 插入单个元素,因此在最坏的情况下,您可能会遇到多次重新分配。

    为了这个例子,考虑初始容量为 2 并在每次重新分配时增加 2 倍的情况。那么

    std::vector<int> vec = { 1 }; 
    vec.push_back(2);             
    vec.push_back(3);                 // need to reallocate, capacity is 4
    vec.push_back(4);                   
    vec.push_back(5);                  // need to reallocate, capacity is 8
    

    你当然可以通过调用来防止不必要的重新分配

    vec.reserve(num_elements_to_push);
    

    不过,如果你从数组中插入,更惯用的方法是使用insert。

    【讨论】:

      【解决方案3】:

      毕竟,他们做同样的事情。不是吗?

      没有。它们不一样。与std::vector::insert 相比,使用std::vector::push_back 的第一种方法将经历多次重新分配。

      insert 将在复制范围之前根据当前的std::vector::capacity 在内部分配内存。有关更多信息,请参阅以下讨论:

      Does std::vector::insert reserve by definition?


      但是性能上有什么区别吗?

      由于上述原因,第二种方法会显示出轻微的性能提升。例如,请参阅下面的快速基准,使用 http://quick-bench.com:

      See online bench-mark

      或者编写一个测试程序来测量性能(如@Some程序员老兄在cmets中提到的)。以下是一个示例测试程序:

      #include <iostream>
      #include <chrono>
      #include <algorithm>
      #include <vector>
      using namespace std::chrono;
      
      class Timer final
      {
      private:
          time_point<high_resolution_clock> _startTime;
      
      public:
          Timer() noexcept
              : _startTime{ high_resolution_clock::now() }
          {}
          ~Timer() noexcept {  Stop(); }
          void Stop() noexcept
          {
              const auto endTime = high_resolution_clock::now();
              const auto start = time_point_cast<microseconds>(_startTime).time_since_epoch();
              const auto end = time_point_cast<microseconds>(endTime).time_since_epoch();
              const auto durationTaken = end - start;
              const auto duration_ms = durationTaken * 0.001;
              std::cout << durationTaken.count() << "us (" << duration_ms.count() << "ms)\n";
          }
      };
      // Method 1: push_back
      void push_back()
      {
          std::cout << "push_backing:    ";
          Timer time{};
          for (auto i{ 0ULL }; i < 1000'000; ++i)
          {
              std::vector<int> vec = { 1 };
              vec.push_back(2);
              vec.push_back(3);
              vec.push_back(4);
              vec.push_back(5);
          }
      }
      // Method 2: insert_range
      void insert_range()
      {
          std::cout << "range-inserting: ";
          Timer time{};
          for (auto i{ 0ULL }; i < 1000'000; ++i)
          {
              std::vector<int> vec = { 1 };
              int arr[] = { 2,3,4,5 };
              vec.insert(std::end(vec), std::cbegin(arr), std::cend(arr));
          }
      }
      
      int main()
      {
          push_back();
          insert_range();
          return 0;
      }
      

      使用我的系统发布构建(MSVS2019:/Ox /std:c++17,AMD Ryzen 7 2700x(8 核,3.70 Ghz) , x64 Windows 10)

      // Build - 1
      push_backing:    285199us (285.199ms)
      range-inserting: 103388us (103.388ms)
      
      // Build - 2
      push_backing:    280378us (280.378ms)
      range-inserting: 104032us (104.032ms)
      
      // Build - 3
      push_backing:    281818us (281.818ms)
      range-inserting: 102803us (102.803ms)
      

      对于给定的场景,std::vector::insert ing 比 std::vector::push_back 快大约 2.7 倍。

      根据实现,看看其他编译器(clang 8.0 和 gcc 9.2)想说什么:https://godbolt.org/z/DQrq51

      【讨论】:

      • 哇!图表显示在这种情况下插入的性能要好得多。
      • @jacobi 是的。另请注意,std::vector:::reserve 将带来同等水平的性能。 See here 因此,如果您事先知道大小,请保留内存,并避免不必要的重新分配。
      • @JeJo:还请注意,从您的链接中:“插入范围时,insert() 的范围版本通常更可取,因为它保留了正确的容量增长行为,与 reserve() 后跟一系列push_back()s。”换句话说,reserve() 将导致重新分配或不执行任何操作,如果它导致重新分配,则在执行push_back() 调用后,您可能最终会得到一个完整或接近完整的向量。这很浪费,因为这意味着您将在不久的将来进行另一个 realloc。
      • @mirabilos 该网站的目的是提供代码 sn-ps 之间的比较,而不是提供准确的 x-y 轴测量值。如果要进行准确的基准测试,应该编写一个测试程序并且必须测量一定的时间。例如,进一步的兴趣,以下是我尝试过的测试程序:godbolt.org/z/DQrq51
      • @JeJo 啊,谢谢。我没有点击链接,因为 SO 上提供的内容“应该足够了”,而且大多数外部内容无论如何都无法在我的浏览器上运行。修改后的图片就可以了,谢谢! (所以,小条 = 更好。)
      【解决方案4】:

      主要的影响因素将是重新分配。 vector 必须为新元素腾出空间。

      考虑这 3 个小片段。

       //pushback
       std::vector<int> vec = {1};
       vec.push_back(2);
       vec.push_back(3);
       vec.push_back(4);
       vec.push_back(5);
      
       //insert
       std::vector<int> vec = {1};
       int arr[] = {2,3,4,5};
       vec.insert(std::end(vec), std::begin(arr), std::end(arr));
      
      
       //cosntruct
       std::vector<int> vec = {1,2,3,4,5};
      

      为了确认图片中的重新分配,在 pushback 和 insert 版本中添加 vec.reserve(5) 后,我们得到以下结果。

      【讨论】:

      • 有趣的是,增加储备如何减少 push_back 和插入到类似水平的时间。
      • 为什么要在insert() 版本中添加reserve()?不过,我在执行此操作时没有得到与您相同的性能影响:quick-bench.com/4ioxDEyzxMl37C7eHfNICKk0Tpc
      • @TedLyngmo 我用 clang 做的。不知道为什么clang的插入速度比较慢。
      • 哇,是的,这真是一个惊喜! ...并使用clang++ + libc++(LLVM) 使情况变得更糟:quick-bench.com/jfcVMGkhgFRI33-2PMPR74cM1G0
      猜你喜欢
      • 2019-05-22
      • 2022-01-10
      • 2017-09-21
      • 2013-10-15
      • 2021-06-18
      • 2023-03-21
      • 1970-01-01
      • 2015-01-15
      • 1970-01-01
      相关资源
      最近更新 更多