是啥让移动对象比复制更快?
Posted
技术标签:
【中文标题】是啥让移动对象比复制更快?【英文标题】:What makes moving objects faster than copying?是什么让移动对象比复制更快? 【发布时间】:2016-08-18 02:18:18 【问题描述】:我听过 Scott Meyers 说“std::move()
不会移动任何东西”......但我不明白它的意思。
所以要指定我的问题,请考虑以下几点:
class Box /* things... */ ;
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
怎么样:
Box box3 = std::move(box1);
我确实了解左值和右值的规则,但我不明白内存中实际发生了什么?它只是以某种不同的方式复制值,共享地址还是什么?更具体地说:是什么让移动比复制更快?
我只是觉得理解这一点会让我明白一切。提前致谢!
编辑:请注意,我不是在询问 std::move()
实现或任何语法内容。
【问题讨论】:
阅读***.com/questions/21358432/… 它更快,因为移动允许源处于无效状态,因此您可以窃取它的资源。例如,如果一个对象持有一个指向一大块已分配内存的指针,则移动可以简单地窃取指针,而副本必须分配自己的内存并复制整个内存块。 复制一个对象意味着你必须把它的内容复制到内存中。假设您有一个包含 2Gb 数据的向量。如果复制向量,这些 2Gb 必须复制到内存中,这需要时间。移动意味着内存中的数据保持原样。只有对该数据的引用从旧对象移动到您要移动到的对象。 @ user1488118 我确实在某处读过它,这对我来说是有意义的,直到我读到向量已满时它会重新分配新内存,并且旧向量中的对象将被移动到新分配的内存.. .这只是弄乱了我的理解...... 回复。 “std::move 不移动任何东西” - 他说std::move(box1);
不移动任何东西;但是Box b = std::move(box1);
确实移动了一些东西。区别在于运动是由b
的初始化执行的,而不是由std::move
的调用。
【参考方案1】:
一切都与实施有关。考虑简单的字符串类:
class my_string
char* ptr;
size_t capacity;
size_t length;
;
copy 的语义要求我们制作字符串的完整副本,包括在动态内存中分配另一个数组并在那里复制 *ptr
内容,这很昂贵。
move 的语义只要求我们将指针本身的值传递给新对象,而不需要复制字符串的内容。
当然,如果类不使用动态内存或系统资源,那么移动和复制在性能方面没有区别。
【讨论】:
这确实有道理,但我读到当向量已满时,它会重新分配新内存,并且旧向量中的对象将被移动(不复制)到新分配的内存......这让我很困惑。 .. @WLION:这有什么令人困惑的地方?我无法想象复制比移动便宜的任何场景。 @WLION 向量本身在某种意义上是指向对象的指针的集合,虽然存储指针的向量可能必须复制到新区域,但这些指针本身不必更改.它们仍然指向存储在相同原始空间中的相同对象。 @Havenard 我在 Visual Studio 中测试过,例如一个大小为 2、容量为 2 的向量,然后我用 std::move() push_back 使向量重新分配......当我访问矢量元素我不必取消引用它们来获取对象的值 @Havenard 向量不是指针的集合。 C++ 标准将其定义为一个连续的内存块,包含对象,很像数组。对象本身的移动是为了消除深拷贝对支持移动语义的对象的性能损失。如果无法移动,则它只是复制对象。【参考方案2】:作为@gudokanswered before,一切都在实现中......然后在用户代码中。
实现
假设我们正在讨论为当前类分配值的复制构造函数。
您将提供的实现将考虑两种情况:
-
参数是一个左值,所以你不能触摸它,根据定义
该参数是一个 r 值,因此,隐含地,在您使用它之后,临时变量的寿命不会更长,因此,您可以窃取其内容,而不是复制其内容
两者都是使用重载实现的:
Box::Box(const Box & other)
// copy the contents of other
Box::Box(Box && other)
// steal the contents of other
轻类的实现
假设您的类包含两个整数:您不能窃取它们,因为它们是普通的原始值。唯一看起来像窃取的是复制值,然后将原始值设置为零,或类似的东西......这对于简单的整数没有意义.为什么要做这些额外的工作?
所以对于轻值类,实际上提供两种具体的实现,一种用于左值,一种用于右值,是没有意义的。
只提供左值实现就绰绰有余了。
较重类的实现
但在某些重类(即 std::string、std::map 等)的情况下,复制意味着潜在的成本,通常在分配中。因此,理想情况下,您希望尽可能避免它。这就是窃取临时数据变得有趣的地方。
假设您的 Box 包含指向 HeavyResource
的原始指针,复制成本很高。代码变为:
Box::Box(const Box & other)
this->p = new HeavyResource(*(other.p)) ; // costly copying
Box::Box(Box && other)
this->p = other.p ; // trivial stealing, part 1
other.p = nullptr ; // trivial stealing, part 2
很明显,一个构造函数(复制构造函数,需要分配)比另一个(移动构造函数,只需要分配原始指针)慢得多。
什么时候“偷”是安全的?
事情是:默认情况下,编译器只会在参数是临时参数时调用“快速代码”(它有点微妙,但请耐心等待......)。
为什么?
因为编译器可以保证你可以毫无问题地从某个对象中窃取只有如果该对象是临时的(或者无论如何都会很快被销毁)。对于其他对象,窃取意味着您突然拥有一个有效但处于未指定状态的对象,该对象仍可在代码中进一步使用。可能导致崩溃或错误:
Box box3 = static_cast<Box &&>(box1); // calls the "stealing" constructor
box1.doSomething(); // Oops! You are using an "empty" object!
但有时,您需要性能。那么,你是怎么做到的呢?
用户代码
正如你所写:
Box box1 = some_value;
Box box2 = box1; // value of box1 is copied to box2 ... ok
Box box3 = std::move(box1); // ???
box2 发生的情况是,由于 box1 是左值,因此调用了第一个“慢”复制构造函数。这是正常的 C++98 代码。
现在,对于 box3,发生了一些有趣的事情:std::move 确实返回了相同的 box1,但作为 r 值引用,而不是 l 值。所以这行:
Box box3 = ...
... 不会在 box1 上调用复制构造函数。
它将在box1上调用INSTEAD窃取构造函数(官方称为move-constructor)。
并且由于您对 Box 的移动构造函数的实现确实“窃取”了 box1 的内容,在表达式的末尾,box1 处于有效但未指定的状态(通常为空),并且 box3 包含(previous) box1 的内容。
移出班级的有效但未指定的状态如何?
当然,在左值上编写 std::move 意味着您承诺不再使用该左值。或者你会非常非常小心地这样做。
引用 C++17 标准草案(C++11 为:17.6.5.15):
20.5.5.15 库类型的移动状态 [lib.types.movedfrom]
在 C++ 标准库中定义的类型的对象可以从 (15.8) 中移出。移动操作可以显式指定或隐式生成。除非另有说明,否则此类移出的对象应处于有效但未指定的状态。
这是关于标准库中的类型的,但这是您自己的代码应该遵循的。
这意味着移出的值现在可以保存任何值,可以是空值、零值或某个随机值。例如。据你所知,如果实施者认为这是正确的解决方案,你的字符串“Hello”将变成一个空字符串“”,或者变成“Hell”,甚至是“Goodbye”。不过,它仍然必须是一个有效的字符串,并尊重其所有不变量。
因此,最后,除非(某个类型的)实现者在移动后明确承诺特定行为,否则您应该表现得好像您对移出的值(的那种)。
结论
如上所述,std::move 什么都不做。它只告诉编译器:“你看到那个左值了吗?请考虑一下它是右值”。
所以,在:
Box box3 = std::move(box1); // ???
...用户代码(即std::move)告诉编译器参数可以被认为是这个表达式的r值,因此将调用move构造函数。
对于代码作者(和代码审阅者)来说,代码实际上告诉可以窃取 box1 的内容,将其移动到 box3 中。然后,代码作者必须确保不再使用 box1(或非常小心地使用)。这是他们的责任。
但最终,移动构造函数的实现会有所不同,主要是在性能方面:如果移动构造函数实际上窃取了 r 值的内容,那么你会看到不同之处。如果它做了别的什么,那么作者就撒谎了,但这是另一个问题......
【讨论】:
很好的解释,但老实说,我已经理解了你解释的大部分内容......但让我感到困惑的是,POD 不能移动但可以复制(不确定 100%),正如我在上面评论的那样,我也听到 Scott Meyers 在重新分配时谈论移动(不复制)vector 的元素,所以我想我们如何移动元素?移动整个向量我理解它只是交换指针但移动对我没有意义的元素......无论如何+1 @Leo :我完成了解释当移动对于某些类型(主要是仅包含原始整数的简单类)没有意义时的答案。对于您的矢量问题,当矢量将尝试移动每个单独的对象时,如果移动实现不适用于该对象,则移动尝试将导致一个简单的副本。 @hyde :我删除了那些错别字(从原来的基于“operator =”的代码,在它变成构造函数之前)。谢谢! :-) @paercebal:您在答案中提供的解释总是很棒。我真的很喜欢读你写的答案。写得很好的答案!!! @Destructor 感谢您的评论。它确实促使我重新审视答案,并澄清其“空状态”的东西,所以可能想重新阅读它(“搬出班级的有效但未指定的状态呢?”部分,即) 【参考方案3】:std::move()
函数应该被理解为对相应右值类型的强制转换,它启用移动对象而不是复制。
这可能根本没有区别:
std::cout << std::move(std::string("Hello, world!")) << std::endl;
这里,字符串已经是一个右值,所以std::move()
没有改变任何东西。
它可能会启用移动,但仍可能导致复制:
auto a = 42;
auto b = std::move(a);
没有比复制它更有效的创建整数的方法了。
它将导致移动发生的地方是当参数发生时
-
是一个左值或左值引用,
具有移动构造函数或移动赋值运算符,并且
是(隐式或显式)构造或赋值的来源。
即使在这种情况下,实际上移动数据的不是move()
本身,而是构造或赋值。 std:move()
只是允许这种情况发生的演员,即使你有一个左值开始。如果您从右值开始,则可以在没有std::move
的情况下进行移动。我认为这就是迈耶斯声明背后的含义。
【讨论】:
由于内存占用,复制不好?由于删除不必要的对象需要时间,因此移动很糟糕?我说的对吗?以上是关于是啥让移动对象比复制更快?的主要内容,如果未能解决你的问题,请参考以下文章
除了多路复用和服务器推送之外,是啥让 http/2 比 http/1 更快?
是啥让访问 OLAP 多维数据集/数据集市和类似数据结构比访问关系数据库更快?