【问题标题】:Why would I std::move an std::shared_ptr?为什么我要 std::move 一个 std::shared_ptr?
【发布时间】:2017-06-11 18:51:34
【问题描述】:

我一直在查看Clang source code,发现了这个sn-p:

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

我为什么要std::movestd::shared_ptr

转让共享资源的所有权有什么意义吗?

我为什么不这样做呢?

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

【问题讨论】:

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


    【解决方案1】:

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

    【讨论】:

    • 不是过早优化吗?
    • @YSC 如果有人把它放在那里实际测试过。
    • @YSC 过早的优化是邪恶的,如果它使代码更难阅读或维护。这个两者都没有,至少在 IMO。
    • 确实如此。这不是过早的优化。相反,这是编写此函数的明智方法。
    【解决方案2】:

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

    【讨论】:

      【解决方案3】:
      std::shared_ptr

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

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

      那么,详细分析一下这个案例的引用计数动态:

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

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

      1. 进入方法时,shared_ptr参数是copy构造的:ref count atomic increment
      2. 在方法体中,复制 shared_ptr 参数到数据成员中:ref count atomic increment
      3. 退出方法时,shared_ptr 参数被破坏:ref count atomic decrement

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

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

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

      底线:在这种情况下,您只会得到 一个 ref count 原子增量,即只有 一个原子 操作。
      如您所见,这比 两个 原子增量加上 一个 原子减量(总共 三个 原子操作)用于复制情况。

      【讨论】:

      • 另外值得注意的是:为什么他们不只是通过 const 引用传递,并避免整个 std::move 东西?因为传值也可以让你直接传入一个原始指针,并且只会创建一个 shared_ptr。
      • @JosephIreland 因为你不能移动 const 引用
      • @JosephIreland 因为如果您将其称为compilerInstance.setInvocation(std::move(sp));,则不会有增量。您可以通过添加采用 shared_ptr&lt;&gt;&amp;&amp; 的重载来获得相同的行为,但为什么在不需要时重复。
      • @BrunoFerreira 我在回答我自己的问题。你不需要移动它,因为它是一个参考,只需复制它。仍然只有一份而不是两份。他们不这样做的原因是因为它会不必要地复制新构建的 shared_ptrs,例如来自setInvocation(new CompilerInvocation),或如棘轮所述,setInvocation(std::move(sp))。对不起,如果我的第一条评论不清楚,我实际上是在我写完之前不小心发布的,我决定离开它
      【解决方案4】:

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

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

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

      请注意,此技术纯粹用于优化。复制它(如您所建议的)在功能方面同样出色。

      【讨论】:

      • 真的快倍吗?你有这方面的基准吗?
      • @xaviersjs 当 Value 超出范围时,赋值需要一个原子增量,然后是一个原子减量。原子操作可能需要数百个时钟周期。所以是的,它确实慢得多。
      • @Adisak 这是我第一次听说获取和添加操作 (en.wikipedia.org/wiki/Fetch-and-add) 可能需要比基本增量多数百个周期。你有这方面的参考吗?
      • @xaviersjs : stackoverflow.com/a/16132551/4238087 寄存器操作只需几个周期,原子的 100 (100-300) 个周期就可以满足要求。尽管指标来自 2013 年,但似乎仍然如此,尤其是对于多插槽 NUMA 系统。
      • 有时你认为你的代码中没有线程......但随后一些该死的库出现并为你毁了它。最好使用 const 引用和 std::move...,如果您可以...而不是依赖指针引用计数。
      【解决方案5】:

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

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

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

      【讨论】:

      • 正是我想要的。惊讶于其他答案如何忽略这一重要的语义差异。智能指针与所有权有关。
      • 我认为所有权在 lambda 表示法中尤其重要。通过引用共享的 ptr 捕获可能不会对其引用计数器做出贡献,并且在代码退出和 ptr 销毁后,您将拥有带有悬空指针的 lambda。
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

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

        tl;dr

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

        • std::move 比传值快 4 倍
        • std::move 比 pass-by-reference 稍微

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

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

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

        • std::move 比传值快 1-2 倍
        • std::move 比 pass-by-reference 稍微

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

        所以最后,我真的不知道该怎么做。请扔飞镖,告诉我我做错了什么,提供一些解释。

        【讨论】:

          【解决方案8】:

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

          在我的 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));
          
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-07-28
            • 2020-11-04
            • 2014-02-16
            • 1970-01-01
            • 2015-06-21
            • 2020-09-01
            相关资源
            最近更新 更多