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

Why would I std::move an std::shared_ptr?

我一直在浏览 Clang source code 并找到了这个片段:

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

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

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

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

相反,对 std::shared_ptrcopy 操作调用 atomic 引用计数增加(即不仅仅是 ++RefCount整数 RefCount 数据成员,但例如在 Windows 上调用 InterlockedIncrement),这比仅窃取 pointers/state.[=] 更 昂贵 28=]

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

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

如果您按值传递 sp,然后在 CompilerInstance::setInvocation 方法中进行 copy,您有:

  1. 进入方法时,shared_ptr参数是拷贝构造的:ref count atomic increment.
  2. 在方法体内,您 copy shared_ptr 参数到数据成员中:ref count atomic 增量.
  3. 退出方法时,shared_ptr参数被析构:ref count atomic decrement.

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

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

  1. 进入方法时,shared_ptr参数是拷贝构造的:ref count atomic increment.
  2. 在方法体内,您 std::moveshared_ptr 参数放入数据成员中:ref count 不会 改变!您只是在窃取 pointers/state:不涉及昂贵的原子引用计数操作。
  3. 退出方法时,shared_ptr参数被析构;但是由于您在第 2 步中移动,因此没有任何东西可以破坏,因为 shared_ptr 参数不再指向任何东西。同样,在这种情况下不会发生原子递减。

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

我认为其他答案没有强调的一点是speed

std::shared_ptr 引用计数是 atomic。增加或减少引用计数 需要 atomic 增加或减少 。这比 non-atomic increment/decrement 慢 一百倍,更不用说如果我们递增和递减同一个计数器我们最终得到确切的数字,在此过程中浪费了大量时间和资源。

通过移动 shared_ptr 而不是复制它,我们 "steal" atomic 引用计数并使另一个 shared_ptr 无效。 "stealing" 引用计数不是 atomic,它比复制 shared_ptr 快一百倍(并导致 atomic参考增量或减量)。

请注意,此技术纯粹用于优化。复制它(如您所建议的)一样好 functionality-wise.

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

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

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

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

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

tl;博士

对于无优化(-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,所以随机部分必须支配该例程。

所以最后,我不太确定这是怎么回事。请扔飞镖,告诉我我做错了什么,提供一些解释。

很遗憾,我没有阅读@yano 的回答。所以我做了自己的基准测试。遗憾的是没有人试图验证这里的假设。我的结果与 yanos 相似,从某种意义上说,改进 far 相差数百倍。

在我的 Macbook Air 上 move 倍(g++ 以及 clang++ -std=c++17 -O3 -DNDEBUG)。如果您发现基准测试有问题,请告诉我。

#include <chrono>
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
using namespace std::chrono;


int COUNT = 50'000'000;

struct TimeIt
{
    system_clock::time_point start;
    TimeIt() {
        start = system_clock::now();
    }
    ~TimeIt() {
        auto runtime = duration_cast<milliseconds>(system_clock::now()-start).count();
        cout << runtime << " ms" << endl;
    }

};

void benchmark_copy(const vector<shared_ptr<int>> &vec_src)
{
    cout << "benchmark_copy" << endl;
    vector<shared_ptr<int>> vec_dst;
    vec_dst.reserve(COUNT);
    TimeIt ti;
    for(auto &sp : vec_src)
        vec_dst.emplace_back(sp);
}

void benchmark_move(vector<shared_ptr<int>> &&vec_src)
{
    cout << "benchmark_move" << endl;
    vector<shared_ptr<int>> vec_dst;
    vec_dst.reserve(COUNT);
    TimeIt ti;
    for(auto &sp : vec_src)
        vec_dst.emplace_back(move(sp));

}

int main (int arg, char **argv){

    vector<shared_ptr<int>> vec;
    for (int i = 0; i < COUNT; ++i)
        vec.emplace_back(new int);

    benchmark_copy(vec);
    benchmark_move(move(vec));

}