【问题标题】:resize versus push_back in std::vector : does it avoid an unnecessary copy assignment?在 std::vector 中调整大小与 push_back :它是否避免了不必要的复制分配?
【发布时间】:2009-12-14 19:03:39
【问题描述】:

当从std::vector调用方法push_back时,它的大小加一,意味着创建一个新实例,然后你传递的参数将被复制到这个最近创建的元素中,对吧?示例:

myVector.push_back(MyVectorElement());

那么,如果我想简单地使用元素的默认值来增加向量的大小,那么使用resize 方法不是更好吗?我的意思是这样的:

myVector.resize(myVector.size() + 1);

据我所知,这将完成完全相同的事情,但会避免元素属性的完全不必要的分配副本。

这个推理是正确的还是我遗漏了什么?

【问题讨论】:

  • 如果你拼写出来,为什么还要使用首字母缩略词? :P
  • 能够在以后的问题中使用它,而不必再次拼写出来...... ;)
  • 问这个问题时,我想知道推理是否正确。当使用带有创建成本高的元素的向量时,我确实使用 Boost 智能指针指向实际实例...
  • 似乎大多数答案早于 c++11,它有 vector::emplace_back。我打算添加一个 C++98 标记,但后来我意识到有一个答案是指 emplace_back....

标签: c++ stl vector


【解决方案1】:

至少对于 GCC,您使用哪个并不重要(结果如下)。但是,如果您到了不得不担心的地步,您应该使用指针或(甚至更好)某种形式的smart pointers.。我当然会推荐the ones in the boost library

如果您想知道在实践中使用哪个更好,我建议push_backreserve 因为 resize 会在每次调用向量时调整向量的大小,除非它的大小与请求的大小相同。 push_back 和 reserve 只会在需要时调整向量的大小。这是一件好事,因为如果您想将矢量调整为 size+1,它可能已经在 size+20,因此调用 resize 不会提供任何好处。

测试代码

#include <iostream>
#include <vector>

class Elem{
    public:
        Elem(){
            std::cout << "Construct\n";
        }
        Elem(const Elem& e){
            std::cout << "Copy\n";
        }
        ~Elem(){
            std::cout << "Destruct\n";
        }   
};


int main(int argc, char* argv[]){
    {
        std::cout << "1\n";
        std::vector<Elem> v;
        v.push_back(Elem());
    }

    {
        std::cout << "\n2\n";
        std::vector<Elem> v;
        v.resize(v.size()+1);
    }
}

测试输出

1
Construct
Copy
Destruct
Destruct

2
Construct
Copy
Destruct
Destruct

【讨论】:

  • 我在 VS2008 中进行了同样的测试,得到了同样的结果。我忽略了调整大小也会执行完全相同的复制操作。 “米特破案”! ;)
  • 除了创建智能指针容器,还可以使用 Boost pointer containers 来获得与标准容器几乎相同的行为(容器负责删除其内容),但在内部使用智能指针实际数据。
  • “我当然会推荐 boost 库中的那些。”不再是最新的了;)
【解决方案2】:

我发现myVector.push_back(MyVectorElement()); 更直接且更易于阅读。

问题是,resize 不只是在这些地方调整数组和默认构造元素的大小;这就是它的默认值。它实际上需要第二个参数,这是每个新元素将被复制的内容,默认为T()。本质上,您的两个代码示例完全相同相同。

【讨论】:

  • 除了 push_back() 可能会分配更多空间。执行 push_back() 二十次可能在内部只调用 resize() 一次。调用 resize() 二十次可能意味着重新分配底层存储二十次()。
  • @GMan:我也发现push_back 更清晰。我只是想知道使用resize 是否会保存这些复制操作。但现在我发现情况并非如此,所以我会回到前者。
  • @Martin York:当然,使用resize 增加大小可能会触发底层数组的实际“调整大小”和新元素的实例化。但我几乎可以肯定底层数组的大小不会每次都改变;仅在需要时。
【解决方案3】:

关于 Yacobi 已接受答案的测试代码的 c++0x 视角:

  1. 在类中添加move构造函数:

    Elem(Elem&& e) { std::cout << "Move\n"; }
    

    使用 gcc 我得到“移动”而不是“复制”作为push_back 的输出,这通常效率更高。

  2. 甚至稍微好一点 emplace操作(取同 作为构造函数的参数):

    v.emplace_back()

测试输出:

1
Construct
Destruct

2
Construct
Copy
Destruct
Destruct

【讨论】:

    【解决方案4】:

    在 EA(电子艺术),这被认为是一个大问题,以至于他们编写了自己的 STL 版本EASTL,其中包括在他们的vector 类中的push_back(void)

    【讨论】:

      【解决方案5】:

      你是对的,push_back 不能避免至少一个副本,但我认为你担心的是错误的事情,但是resize 也不一定会表现得更好(它复制了它的第二个参数的值,默认值到一个默认的临时构造)。

      vector 不是复制成本高昂的对象的正确容器。 (几乎)任何push_backresize 都可能导致vector 的每个当前成员以及任何新成员都被复制。

      【讨论】:

        【解决方案6】:

        当您执行 push_back() 时,该方法会检查底层存储区域以查看是否需要空间。如果需要空间,它将为所有元素分配一个新的连续区域并将数据复制到新区域。

        BUT:新分配空间的大小不仅仅是大了一个元素。它使用一个漂亮的小算法来增加空间(我不认为该算法被定义为标准的一部分,但它通常使分配的空间增加一倍)。因此,如果您推送大量元素,实际上只有一小部分元素会导致底层空间被重新分配。

        要手动增加分配空间,您有两种选择:

        • reserve()

          这增加了底层存储空间,而不向向量添加元素。因此,未来的push_back() 呼叫不太可能需要增加空间。

        • resize()

          这实际上是在向量中添加/删除元素以使其大小正确。

        • capacity()

          是在底层存储需要重新分配之前可以存储的元素总数。因此如果capacity() &gt; size() push_back 不会导致向量存储被重新分配。

        【讨论】:

          【解决方案7】:
          myVector.resize(myVector.size() + 1);
          

          将调用 MyVectorElement 的空构造函数。你想达到什么目的?为了在向量中保留空间(并节省内存分配开销),有 reserve() 方法。你无法避免构造函数。

          【讨论】:

            【解决方案8】:

            当您调用push_back 时,假设不需要调整底层存储的大小,vector 类将使用“placement new”运算符就地复制构造新元素。向量中的元素在复制构造之前将默认构造。

            当您拨打resize 时,会发生几乎完全相同的顺序。 vector 分配存储空间,然后通过placement new 将默认值复制到每个新位置。

            结构如下:

            ::new (p) _T1(_Val);
            

            其中p 是指向向量存储的指针,_T1 是存储在向量中的类型,_Val 是“默认值”参数(默认为_T1())。

            简而言之,resize 和 push_back 在幕后做同样的事情,速度差异可能是由于多次内部分配、多次数组边界检查和函数调用开销。时间和内存复杂度是一样的。

            【讨论】:

              【解决方案9】:

              显然您担心效率和性能。

              std::vector 实际上是一个非常好的执行者。如果您大致知道它可能有多大,请使用 reserve 方法预分配空间。显然,这是以可能浪费内存为代价的,但如果您经常使用 push_back,它可能会对性能产生相当大的影响。

              我相信它的实现取决于预先为向量保留多少内存(如果有的话),或者在添加元素时为将来使用保留多少内存。最坏的情况就是您所说的 - 一次只增长一个元素。

              在您的应用中尝试一些性能测试,方法是在没有保留和有保留的情况下进行比较。

              【讨论】:

                【解决方案10】:

                push_back:您创建对象并将其复制到向量中。
                resize:向量使用默认构造函数创建对象并将其复制到向量。

                速度差异:您必须测试您的 STL 实现和编译器,但我认为这无关紧要。

                【讨论】:

                  【解决方案11】:

                  我怀疑实际答案很大程度上是 STL 实现和使用的编译器的函数,但是,“resize”函数具有原型 (ref)

                  void resize( size_type num, TYPE val = TYPE() );
                  

                  这意味着 val 是默认构造的,并通过放置 new 和复制构造函数复制到新分配的(或可能先前分配但未使用的)空间中。因此,这两个操作都需要相同的操作序列:

                  1. 调用默认构造函数
                  2. 分配空间
                  3. 通过复制构造函数初始化

                  推迟到更清晰和更通用(就 STL 容器而言)push_back 可能更好,而不是应用过早的优化 - 如果分析器将 push_back 突出显示为热点,那么最可能的原因是内存分配是最好通过明智地使用储备来解决。

                  【讨论】:

                    猜你喜欢
                    • 2019-11-21
                    • 1970-01-01
                    • 2011-01-17
                    • 2013-04-14
                    • 2020-10-11
                    • 2012-07-10
                    • 2012-12-28
                    • 2018-04-30
                    • 2010-11-12
                    相关资源
                    最近更新 更多