我为什么要std :: move一个std :: shared_ptr?


147

我一直在浏览Clang源代码,发现以下代码段:

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}

我为什么要std::move一个std::shared_ptr

在共享资源上转移所有权有什么意义吗?

我为什么不这样做呢?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}

Answers:


136

我认为其他答案没有足够强调的一件事是速度

std::shared_ptr参考计数是原子的。增加或减少引用计数需要原子递增或递减。这比非原子增量/减量了一百倍,更不用说如果我们递增和递减相同的计数器,我们将得到确切的数字,这会浪费大量的时间和资源。

通过移动shared_ptr而不是复制它,我们“窃取”了原子引用计数,并且使另一个无效shared_ptr。“窃取”引用计数不是原子的,它比复制计数快100倍shared_ptr(并导致原子引用增加或减少)。

请注意,此技术仅用于优化。复制它(按照您的建议)在功能上也不错。


5
真的快百倍吗?您有基准吗?
xaviersjs '19

1
@xaviersjs赋值需要原子增量,然后在Value超出范围时进行原子递减。原子操作可能需要数百个时钟周期。是的,确实要慢得多。
阿迪萨克

2
@Adisak是我听说的第一个获取和添加操作(en.wikipedia.org/wiki/Fetch-and-add),它可能比基本增量要花费数百个周期。你有参考吗?
xaviersjs

2
@xaviersjs:stackoverflow.com/a/16132551/4238087寄存器操作为几个周期,原子的100个周期(100-300)适合。尽管指标来自2013年,但对于多路NUMA系统尤其如此。
Russianfool

1
有时您认为代码中没有线程...但是随后出现了一个织补库,并为您破坏了它。如果很明显,可以使用const引用和std :: move ...,而不是依赖指针引用计数。
Erik Aronesty

122

通过使用该方法,move您可以避免增加数量,然后立即减少数量。这样可以为您节省一些昂贵的原子操作。


1
这不是过早的优化吗?
YSC

11
@YSC不管是否有人实际测试过它。
OrangeDog

19
@YSC如果过早优化会使代码难以阅读或维护,则它是有害的。至少IMO,这一项都不做。
Angew不再为SO

17
确实。这不是过早的优化。相反,这是编写此函数的明智方式。
Lightness Races in Orbit

60

移动操作(如移动的构造函数)为std::shared_ptr便宜的,因为它们基本上是“偷指针”(从源到目的地;更精确地说,整个状态控制块被从源到目的地“偷”,包括引用计数信息) 。

取而代之的是,对调用原子引用计数的复制操作会增加(即,不仅在整数数据成员上,而且例如在Windows上进行调用)也比仅窃取指针/状态要昂贵std::shared_ptr++RefCountRefCountInterlockedIncrement

因此,详细分析这种情况下的引用计数动态:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);

如果您sp按值传递,然后在方法内部进行复制CompilerInstance::setInvocation,则您将:

  1. 输入方法时,该shared_ptr参数将被复制构造:ref count 原子 增量
  2. 在方法的身体,你复制shared_ptr参数为数据成员:引用计数原子 增量
  3. 退出方法时,shared_ptr参数将被破坏:ref count atomic decrement

您有两个原子增量和一个原子递减,总共进行了三个 原子操作。

相反,如果您先按shared_ptr值传递参数,然后std::move在方法内部传递(如Clang的代码所述),则您将:

  1. 输入方法时,该shared_ptr参数将被复制构造:ref count 原子 增量
  2. 在方法的身体,你std::moveshared_ptr参数为数据成员:引用计数并没有改变!您只是在窃取指针/状态:不涉及昂贵的原子引用计数操作。
  3. 退出方法时,shared_ptr参数将被破坏;但是自从您进入第2步以来,没有什么可破坏的,因为该shared_ptr参数不再指向任何东西。同样,在这种情况下不会发生原子递减。

底线:在这种情况下,您只会获得一个引用计数原子增量,即只有一个原子操作。
如您所见,对于复制案例,这比两个原子增量加一个原子递减(总共三个原子操作)要好得多


1
还值得注意的是:为什么不通过const引用传递它们,而避免整个std :: move东西呢?由于按值传递还使您可以直接传递原始指针,因此只会创建一个shared_ptr。
约瑟夫·爱尔兰

@JosephIreland因为您无法移动const引用
Bruno Ferreira

2
@JosephIreland,因为如果您按原样调用它,compilerInstance.setInvocation(std::move(sp));则不会增加。通过添加一个需要重载的重载,您可以得到相同的行为,shared_ptr<>&&但是为什么不需要时重载。
棘轮怪胎

2
@BrunoFerreira我在回答自己的问题。您无需移动它,因为它是参考,只需复制它即可。仍然只有一个副本,而不是两个副本。他们之所以不这样做,是因为它将不必要地复制新构造的shared_ptrs,例如from setInvocation(new CompilerInvocation)或如棘轮所提到的setInvocation(std::move(sp))。抱歉,如果我的第一条评论不清楚,我实际上是在写完文章之前无意中将其发布了,因此我决定将其保留
Joseph Ireland,

22

复制a shared_ptr涉及复制其内部状态对象指针并更改引用计数。移动它仅涉及交换指向内部引用计数器和拥有对象的指针,因此速度更快。


16

在这种情况下使用std :: move有两个原因。大多数答复都涉及速度问题,但忽略了更清楚地显示代码意图的重要问题。

对于std :: shared_ptr,std :: move明确表示该指针的所有权转移,而简单的复制操作会添加一个附加所有者。当然,如果原始所有者随后放弃了所有权(例如通过销毁其std :: shared_ptr),则所有权转让已经完成。

当您通过std :: move转移所有权时,很明显发生了什么。如果您使用普通副本,则在确认原始所有者立即放弃所有权之前,预期的操作不是转移。另外,更有效的实现是可能的,因为所有权的原子转移可以避免临时状态,即所有者数增加了一个(并且随之而来的参考计数也发生了变化)。


正是我要的东西。惊讶于其他答案如何忽略了这一重要的语义差异。明智的指示是关于所有权的。
qweruiop

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.