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

sdg*_*sdh 133 c++ smart-pointers shared-ptr move-semantics c++11

我一直在查看Clang源代码,我找到了这个片段:

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = std::move(Value);
}
Run Code Online (Sandbox Code Playgroud)

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

在共享资源上转移所有权是否有任何意义?

为什么我不这样做呢?

void CompilerInstance::setInvocation(
    std::shared_ptr<CompilerInvocation> Value) {
  Invocation = Value;
}
Run Code Online (Sandbox Code Playgroud)

Bo *_*son 118

通过使用move您可以避免增加,然后立即减少共享的数量.这可能会在使用次数上为您节省一些昂贵的原子操作.

  • @YSC过早优化是邪恶的,如果它使代码更难阅读或维护.至少IMO没有这个. (17认同)
  • 确实.这不是一个不成熟的优化.相反,它是编写此函数的明智方法. (15认同)
  • @YSC不是,如果谁把它放在那里实际测试它. (11认同)
  • 不是过早优化吗? (2认同)

Dav*_*aim 116

我认为其他答案没有强调的一点就是速度.

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

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

请注意,此技术仅用于优化.复制它(如你所建议的)就像功能一样好.

  • 做了一个小基准测试,发现它的速度大约快了***三***倍。[复制成本与移动 std::shared_ptr](/sf/ask/4903778001/#70054410) (4认同)
  • @Adisak 这是我第一次听说取和添加操作 (https://en.wikipedia.org/wiki/Fetch-and-add) 可能比基本增量花费数百个周期。你有这方面的参考吗? (3认同)
  • @xaviersjs:/sf/answers/1129278601/ 由于寄存器操作需要几个周期,因此原子的 100 个(100-300)个周期就符合要求。尽管指标来自 2013 年,但这似乎仍然如此,尤其是对于多插槽 NUMA 系统。 (3认同)
  • 有时你认为你的代码中没有线程......但随后一些该死的库出现并毁了你的代码。最好使用 const 引用和 std::move...如果你可以清楚且明显地...而不是依赖指针引用计数。 (3认同)
  • 真的快一百倍了吗?您有基准吗? (2认同)
  • @xaviersjs 当 Value 超出范围时,赋值需要一个原子增量,然后是一个原子减量。原子操作可能需要数百个时钟周期。所以是的,它确实要慢得多。 (2认同)

Mr.*_*C64 57

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

而是在调用原子引用计数时复制操作增加(即不仅仅是在整数数据成员上,而是在Windows上调用),这比仅仅窃取指针/状态更昂贵.std::shared_ptr++RefCountRefCountInterlockedIncrement

因此,详细分析此案例的引用计数动态:

// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);
Run Code Online (Sandbox Code Playgroud)

如果您通过sp值传递然后在方法中复制CompilerInstance::setInvocation,则您具有:

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

您有两个原子增量和一个原子减量,总共三个 原子操作.

相反,如果您通过shared_ptr值传递参数然后std::move在方法内部(正如在Clang的代码中正确完成),您有:

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

底线:在这种情况下,您只得到一个 ref计数原子增量,即只有一个原子操作.
正如您所看到的,这比复制案例的两个原子增量加上一个原子减量(总共三个原子操作)要好得多.

  • @JosephIreland因为如果你把它称为`compilerInstance.setInvocation(std :: move(sp));`那么就没有**增量**.您可以通过添加一个带有`shared_ptr <> &&`的重载来获得相同的行为,但是为什么在不需要时重复. (2认同)
  • @BrunoFerreira我正在回答我自己的问题.你不需要移动它,因为它是一个引用,只需复制它.仍然只有一个副本而不是两个.他们不这样做的原因是因为它会不必要地新建shared_ptrs,例如,从`setInvocation(新CompilerInvocation)复制',或者作为棘轮提到,`setInvocation(STD ::移动(SP))`.对不起,如果我的第一条评论不清楚,我实际上是在写完之前偶然发布的,我决定离开它 (2认同)

Sin*_*all 21

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


Ste*_*eel 14

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

对于std :: shared_ptr,std :: move明确表示指向者的所有权转移,而简单的复制操作则添加额外的所有者.当然,如果原所有者随后放弃了他们的所有权(例如允许他们的std :: shared_ptr被销毁),那么就完成了所有权的转移.

当您使用std :: move转移所有权时,显而易见的是发生了什么.如果您使用普通副本,则在您验证原始所有者是否立即放弃所有权之前,预期的操作并不明显.作为奖励,可以实现更有效的实施,因为所有权的原子转移可以避免所有者数量增加1的临时状态(以及随之而来的参考计数变化).


yan*_*ano 9

由于这些答案都没有提供实际的基准,我想我应该尝试提供一个。然而,我想我已经让自己比开始时更加困惑了。我试图提出一个测试,该测试可以测量shared_ptr<int>按值传递、按引用传递、使用std::move、对该值执行添加操作并返回结果。我使用两组测试进行了多次(一百万次)。第一组在 中添加了一个常量值shared_ptr<int>,另一组在 [0, 10] 范围内添加了一个随机值。我认为恒定值添加将是重度优化的候选者,而随机值测试则不然。这或多或少是我所看到的,但是执行时间的极端差异使我相信此测试程序的其他因素/问题是导致执行时间差异的因素,而不是移动语义。

太长了;博士

对于没有优化 ( -O0),不断添加

  • std::move比按值传递快约 4 倍
  • std::move比按引用传递稍微慢一些

对于高度优化 ( -O3),不断添加

  • std::move比按值传递快70-90,000
  • std::move比引用传递略快(1-1.4 倍)

对于不优化 ( -O0),随机添加

  • std::move比按值传递快 1-2 倍
  • std::move比按引用传递稍微慢一些

对于高度优化 ( -O3),随机添加

  • std::move比按值传递快 1-1.3 倍(比没有优化稍差)
  • std::move本质上与引用传递相同

最后,测试

#include <memory>
#include <iostream>
#include <chrono>
#include <ctime>
#include <random>

constexpr auto MAX_NUM_ITS = 1000000;

// using random values to try to cut down on massive compiler optimizations
static std::random_device RAND_DEV;
static std::mt19937 RNG(RAND_DEV());
static std::uniform_int_distribution<std::mt19937::result_type> DIST11(0,10);

void CopyPtr(std::shared_ptr<int> myInt)
{
    // demonstrates that use_count increases with each copy
    std::cout << "In CopyPtr: ref count = " << myInt.use_count() << std::endl;
    std::shared_ptr<int> myCopyInt(myInt);
    std::cout << "In CopyPtr: ref count = " << myCopyInt.use_count() << std::endl;
}

void ReferencePtr(std::shared_ptr<int>& myInt)
{
    // reference count stays the same until a copy is made
    std::cout << "In ReferencePtr: ref count = " << myInt.use_count() << std::endl;
    std::shared_ptr<int> myCopyInt(myInt);
    std::cout << "In ReferencePtr: ref count = " << myCopyInt.use_count() << std::endl;
}

void MovePtr(std::shared_ptr<int>&& myInt)
{
    // demonstrates that use_count remains constant with each move
    std::cout << "In MovePtr: ref count = " << myInt.use_count() << std::endl;
    std::shared_ptr<int> myMovedInt(std::move(myInt));
    std::cout << "In MovePtr: ref count = " << myMovedInt.use_count() << std::endl;
}

int CopyPtrFastConst(std::shared_ptr<int> myInt)
{
    return 5 + *myInt;
}

int ReferencePtrFastConst(std::shared_ptr<int>& myInt)
{
    return 5 + *myInt;
}

int MovePtrFastConst(std::shared_ptr<int>&& myInt)
{
    return 5 + *myInt;
}

int CopyPtrFastRand(std::shared_ptr<int> myInt)
{
    return DIST11(RNG) + *myInt;
}

int ReferencePtrFastRand(std::shared_ptr<int>& myInt)
{
    return DIST11(RNG) + *myInt;
}

int MovePtrFastRand(std::shared_ptr<int>&& myInt)
{
    return DIST11(RNG) + *myInt;
}

void RunConstantFunctions(std::shared_ptr<int> myInt)
{
    std::cout << "\nIn constant funcs, ref count = " << myInt.use_count() << std::endl;
    // demonstrates speed of each function
    int sum = 0;

    // Copy pointer
    auto start = std::chrono::steady_clock::now();
    for (auto i=0; i<MAX_NUM_ITS; i++)
    {
        sum += CopyPtrFastConst(myInt);
    }
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> copyElapsed = end - start;
    std::cout << "CopyPtrConst sum = " << sum << ", took " << copyElapsed.count() << " seconds.\n";

    // pass pointer by reference
    sum = 0;
    start = std::chrono::steady_clock::now();
    for (auto i=0; i<MAX_NUM_ITS; i++)
    {
        sum += ReferencePtrFastConst(myInt);
    }
    end = std::chrono::steady_clock::now();
    std::chrono::duration<double> refElapsed = end - start;
    std::cout << "ReferencePtrConst sum = " << sum << ", took " << refElapsed.count() << " seconds.\n";

    // pass pointer using std::move
    sum = 0;
    start = std::chrono::steady_clock::now();
    for (auto i=0; i<MAX_NUM_ITS; i++)
    {
        sum += MovePtrFastConst(std::move(myInt));
    }
    end = std::chrono::steady_clock::now();
    std::chrono::duration<double> moveElapsed = end - start;
    std::cout << "MovePtrConst sum = " << sum << ", took " << moveElapsed.count() <<
        " seconds.\n";

    std::cout << "std::move vs pass by value: " << copyElapsed / moveElapsed << " times faster.\n";
    std::cout << "std::move vs pass by ref:   " << refElapsed / moveElapsed << " times faster.\n";
}

void RunRandomFunctions(std::shared_ptr<int> myInt)
{
    std::cout << "\nIn random funcs, ref count = " << myInt.use_count() << std::endl;
    // demonstrates speed of each function
    int sum = 0;

    // Copy pointer
    auto start = std::chrono::steady_clock::now();
    for (auto i=0; i<MAX_NUM_ITS; i++)
    {
        sum += CopyPtrFastRand(myInt);
    }
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> copyElapsed = end - start;
    std::cout << "CopyPtrRand sum = " << sum << ", took " << copyElapsed.count() << " seconds.\n";

    // pass pointer by reference
    sum = 0;
    start = std::chrono::steady_clock::now();
    for (auto i=0; i<MAX_NUM_ITS; i++)
    {
        sum += ReferencePtrFastRand(myInt);
    }
    end = std::chrono::steady_clock::now();
    std::chrono::duration<double> refElapsed = end - start;
    std::cout << "ReferencePtrRand sum = " << sum << ", took " << refElapsed.count() << " seconds.\n";

    // pass pointer using std::move
    sum = 0;
    start = std::chrono::steady_clock::now();
    for (auto i=0; i<MAX_NUM_ITS; i++)
    {
        sum += MovePtrFastRand(std::move(myInt));
    }
    end = std::chrono::steady_clock::now();
    std::chrono::duration<double> moveElapsed = end - start;
    std::cout << "MovePtrRand sum = " << sum << ", took " << moveElapsed.count() <<
        " seconds.\n";

    std::cout << "std::move vs pass by value: " << copyElapsed / moveElapsed << " times faster.\n";
    std::cout << "std::move vs pass by ref:   " << refElapsed / moveElapsed << " times faster.\n";
}

int main()
{
    // demonstrates how use counts are effected between copy and move
    std::shared_ptr<int> myInt = std::make_shared<int>(5);
    std::cout << "In main: ref count = " << myInt.use_count() << std::endl;
    CopyPtr(myInt);
    std::cout << "In main: ref count = " << myInt.use_count() << std::endl;
    ReferencePtr(myInt);
    std::cout << "In main: ref count = " << myInt.use_count() << std::endl;
    MovePtr(std::move(myInt));
    std::cout << "In main: ref count = " << myInt.use_count() << std::endl;

    // since myInt was moved to MovePtr and fell out of scope on return (was destroyed),
    // we have to reinitialize myInt
    myInt.reset();
    myInt = std::make_shared<int>(5);

    RunConstantFunctions(myInt);
    RunRandomFunctions(myInt);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

现场版本在这里

我注意到对于-O0-O3,常量函数都编译为两组标志的相同程序集,都是相对较短的块。这让我认为大部分优化来自调用代码,但我在我的业余汇编知识中并没有真正看到这一点。

随机函数编译成相当多的汇编,即使对于-O3,因此随机部分必须主导该例程。

所以最后,不太确定该怎么做。请向它扔飞镖,告诉我我做错了什么,并提供一些解释。