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

时间:2017-01-26 10:11:36

标签: c++ c++11 shared-ptr smart-pointers move-semantics

我一直在查看https://msdn.microsoft.com/de-de/library/ee825488(v=cs.20).aspx,我发现了这个片段:

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;
}

7 个答案:

答案 0 :(得分:107)

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

答案 1 :(得分:94)

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

std::shared_ptr引用计数是 atomic 。增加或减少引用计数需要原子递增或递减。这比非原子增量/减量一百倍 时间和资源在这个过程中。

通过移动shared_ptr而不是复制它,我们“窃取”原子引用计数,我们使其他shared_ptr无效。 “窃取”引用计数不是 atomic ,它比复制shared_ptr(并导致 atomic 引用增量或减量)快一百倍。

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

答案 2 :(得分:57)

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

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

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

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

如果您按值传递sp,然后在CompilerInstance::setInvocation方法中执行复制,则您有:

  1. 输入方法时,shared_ptr参数是复制构造的:ref count atomic 增量
  2. 在方法正文中,您将<{em}} shared_ptr参数复制到数据成员中:ref count atomic 增量
  3. 退出方法时,shared_ptr参数被破坏:ref count atomic 减量
  4. 您有两个原子增量和一个原子减量,总计三个 原子操作。

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

    1. 输入方法时,shared_ptr参数是复制构造的:ref count atomic 增量
    2. 在方法的正文中,您std::move shared_ptr参数进入数据成员:引用计数更改!你只是在窃取指针/状态:不涉及昂贵的原子引用计数操作。
    3. 退出方法时,shared_ptr参数被破坏;但是因为你在第2步中移动了,所以没有什么可以破坏,因为shared_ptr参数不再指向任何东西了。同样,在这种情况下不会发生原子减量。
    4. 底线:在这种情况下,您只需一个引用计数原子增量,即只需一个原子操作。
      正如您所看到的,这比两个原子增量加上一个原子减量(总共三个 更好 更好 >原子操作)复制案例。

答案 3 :(得分:18)

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

答案 4 :(得分:13)

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

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

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

答案 5 :(得分:2)

至少对于libstdc ++,您应该在移动和赋值上获得相同的性能,因为operator=在传入指针上调用std::move。参见:https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr.h#L384

答案 6 :(得分:0)

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

tl;dr

对于没有优化(-O0),常量添加

  • std::move 比值传递快约 4 倍
  • std::move 比传递引用慢了一点

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

  • std::move 比传值快 70-90
  • 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;
}

live version here

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

随机函数被编译成相当多的汇编,即使是对于 -O3,所以随机部分必须在该例程中占主导地位。

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