为什么STD:要移动STD:共享_指针?

我一直在查看铿锵源代码,我发现了这个片段:

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

我为什么要将ABC0__std::shared_ptr

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

我为什么不这样做呢?

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

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

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

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

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

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

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

如果通过值传递__abc0,然后在__abc1方法中获取__abc2,则有:

  1. 当进入该方法时,__abc0参数是复制构造的:引用计数__abc1__abc2。
  2. 在方法体内部,将abc1__abc0参数__到数据成员中:ref count__abc2__abc3。
  3. 当退出该方法时,__abc0参数被析构:引用计数__abc1__abc2。

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

相反,如果通过值传递__abc0参数,然后在方法内部__abc2(如Clang的代码中正确执行的操作),则会得到:

  1. 当进入该方法时,__abc0参数是复制构造的:引用计数__abc1__abc2。
  2. 在方法体内部,将__abc1参数__abc0到数据成员中:ref count__abc2是否改变?您只是在窃取指针/状态:不涉及昂贵的原子引用计数操作。
  3. 退出该方法时,__abc0参数被破坏;但是,由于您在第2步中移动了,因此没有什么可破坏的,因为__abc0参数不再指向任何内容。同样,在这种情况下不会发生原子衰减。

底线:在这种情况下,您只得到引用计数原子增量,即只有一个原子操作。
正如您所看到的,对于复制情况,这比原子增量加上原子减量(总共原子操作)较好的

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

std::shared_ptr引用计数原子的。增加或减少引用计数需要原子的增量或减量。这是慢一点非原子的递增/递减的一百倍,更不用说如果我们递增和递减相同的计数器,我们最终会得到确切的数字,在此过程中会浪费大量的时间和资源。

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

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

在这种情况下使用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

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

热释光;博士

对于无优化(-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;
}

现场版本在这里

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

随机函数被编译成相当多的汇编,即使对于-O3也是如此,因此随机部分肯定在该例程中占主导地位。

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

不幸的是,我没有读@矢野的答案。所以我做了自己的基准测试。可悲的是,没有人试图验证这里的假设。我的结果与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));


}