【问题标题】:When appending one vector to another, why is moving the elements any cheaper than copying them?将一个向量附加到另一个向量时,为什么移动元素比复制它们便宜?
【发布时间】:2017-04-11 22:22:42
【问题描述】:

假设我有两个向量,srcdst,我想将 src 附加到 dst 的末尾。

我注意到有关此任务的大多数答案都建议这样做:

     dst.insert(dst.end(),
                std::make_move_iterator(src.begin()),
                std::make_move_iterator(src.end()));

关于这个:

    dst.insert(dst.end(), src.begin(), src.end());

据我所知,在这两种情况下,将元素推送(插入)到向量都需要为向量末尾的插入元素分配空间,以确保内存连续性,我假设 copy 和 @在这种情况下,987654328@ 成本是相同的。

移动对象会使它们立即销毁,这是这样做的唯一好处,还是我还缺少其他东西?

编辑: 你能解释一下这两种情况吗:

  1. 向量包含纯数据,例如:int。
  2. 向量包含类对象。

【问题讨论】:

  • 这两行不等价 - 在第一种情况下,插入后您将无法使用 src 中的项目。在这两种情况下,您都将为向量分配内存,但第一个版本将移动项目,而第二个版本将复制它们,并且移动比复制便宜 - 如果您的项目是大(例如向量),这将产生巨大的影响。
  • 搬家通常比复制便宜。为什么这令人惊讶?
  • 是的,添加元素总是需要更多空间。但是进程的内存占用只会为向量增加,如果每个元素都持有指向某个非常大状态的指针,则不会增加。
  • 我试图理解为什么移动它们更有效..我在这两种情况下都再次分配内存?
  • @Baraa 您正在为向量单元分配内存,而不是项目管理的内存。如果您的向量包含string,您将需要在这两种情况下分配n 单元格,但如果您移动,您将只复制char*int(假设基本实现),而如果您复制您将需要重新分配一个新的 char 数组并复制每个 n 字符串的内容。

标签: c++ vector


【解决方案1】:

我假设复制和移动成本是相同的。

你猜错了。

这与向量无关,也与插入无关。这与构造函数之间的相对成本差异有关

ClassT::ClassT(const ClassT& orig);

ClassT::ClassT(ClassT&& orig);

You can always find a move constructor which is cheaper or equal to the copy constructor.

当构造函数接受右值时,它被称为“移动构造函数” 作为参数引用。它没有义务移动任何东西, 类不需要有要移动的资源和“移动” 构造函数'可能无法像允许的那样移动资源 (但可能不明智)参数是 const rvalue 的情况 参考 (const T&&)。

ClassT 可以执行与复制构造函数完全相同的操作。或者ClassT 可以执行更好的移动构造。这意味着如果在 dst 中插入时不需要 src 中对象的原始实例,则应该使用移动操作。

【讨论】:

  • 我几乎可以肯定 :),但为什么在这个例子中它更便宜?
  • @Baraa 我们需要知道向量的value_type 才能回答这个问题。
  • 大多数情况下,您可以实现比复制构造更便宜的移动构造。 可能是错误的,但肯定是太不具体了。
  • @Walter 我正在尝试查找它不适用的类型。但让我更正一下。
  • 对于任何不管理分配内存的类型(直接或间接通过其成员),移动并不比复制便宜。因此,任何 POD 和内置类型。顺便说一句,我不是反对者之一。
【解决方案2】:

确实,如果复制和移动元素的成本相同(在您的示例中,类型为 int 的元素),则没有区别。

移动仅对将数据存储在堆上的元素产生影响,即使用分配的内存(例如,如果元素是 std::stringstd::vector<something>)。在这种情况下,移动或复制元素会产生(可能是巨大的)差异(前提是正确实现/启用了移动构造函数和operator=(value_type&&)),因为移动仅将指针复制到分配的内存,而副本很深:它分配新内存并复制所有数据,包括递归深拷贝(如果适用)。

关于与存储在std::vector 中的数据相关的成本,如果附加元素超出容量,则会产生一些成本。在这种情况下,将调整整个向量的大小,包括移动 所有 其元素。原因是std::vector,按照规范,将其所有元素存储在一个数组中。如果附加容器是您代码中的常见操作,您可能需要考虑其他容器,例如std::liststd::deque

【讨论】:

    【解决方案3】:

    你已经有了答案。但我认为一个简短的程序可以很好地说明它:

    #include <iostream>
    #include <utility>
    #include <vector>
    #include <iterator>
    
    struct expansive {
        static unsigned instances;
        static unsigned copies;
        static unsigned assignments;
        expansive() { ++instances; }
        expansive(expansive const&) { ++copies; ++instances; }
        expansive& operator=(expansive const&) { ++assignments; return *this; }
    };
    
    unsigned expansive::instances = 0;
    unsigned expansive::copies = 0;
    unsigned expansive::assignments = 0;
    
    struct handle {
        expansive *h;
    
        handle() : h(new expansive) { }
        ~handle() { delete h; }
    
        handle(handle const& other) : h(new expansive(*other.h)) { }
        handle(handle&& other) : h(other.h) { other.h = nullptr; }
        handle& operator=(handle const& other) { *h = *other.h; return *this; }
        handle& operator=(handle&& other)  { std::swap(h, other.h); return *this; }
    };
    
    int main() {
    
      {
          std::vector<handle> v1(10), v2(10);
    
          v1.insert(end(v1), begin(v2), end(v2));
    
          std::cout << "When copying there were "
                    << expansive::instances   << " instances of the object with " 
                    << expansive::copies      << " copies and "
                    << expansive::assignments << " assignments made." << std::endl;
    
      }
    
        expansive::instances = expansive::copies = expansive::assignments = 0;
    
      {
        std::vector<handle> v1(10), v2(10);
    
          v1.insert(end(v1), std::make_move_iterator(begin(v2)),
                           std::make_move_iterator(end(v2)));
    
    
          std::cout << "When moving there were "
                    << expansive::instances   << " instances of the object with " 
                    << expansive::copies      << " copies and "
                    << expansive::assignments << " assignments made.\n";
      }
    
        return 0;
    }
    

    expansive 为复制成本很高的资源建模(想象一下打开文件句柄、网络连接等)。那是handle 管理的资源。为了保持程序的正确性,handle 在其自身被复制时仍必须执行昂贵的复制。

    现在,当程序运行时,它会产生以下输出:

    复制时,对象有 30 个实例,有 10 个副本,并且 已完成 0 个任务。移动时有 20 个实例 具有 0 个副本和 0 个分配的对象。

    这是什么意思?这意味着,如果我们只想将句柄转移到另一个容器,我们必须在此过程中做一些非常广泛的工作(与我们尝试转移的资源数量成线性关系)。移动语义在这里拯救了我们。
    通过使用move_iterator,可以直接转移实际昂贵的资源,而不是进行多余的复制。这可以转化为性能的巨大提升。

    【讨论】:

    • 很好的例子,反正他们已经在字符串例子中找到了我 :)
    • @Baraa,我不怪你 :) 只是认为如果有一个示例可以帮助未来的读者轻松追踪正在发生的事情
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-07
    • 1970-01-01
    • 1970-01-01
    • 2018-10-02
    • 1970-01-01
    • 2011-08-13
    • 1970-01-01
    相关资源
    最近更新 更多