我一直在查看Clang源代码,我发现了这个片段:
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;
}
我认为其他答案没有充分强调的一件事是速度。
std::shared_ptr
引用计数是原子的。增加或减少引用计数需要原子递增或递减。这比非原子递增/递减慢一百倍,更不用说如果我们递增和递减相同的计数器,我们最终会得到确切的数字,在此过程中浪费大量时间和资源。
通过移动shared_ptr
而不是复制它,我们"窃取">了原子参考计数,并使其他shared_ptr
无效。"窃取"引用计数不是原子的,它比复制shared_ptr
快一百倍(并导致原子引用递增或递减)。
请注意,此技术仅用于优化。 复制它(如您所建议的)在功能方面同样很好。
通过使用move
可以避免增加然后立即减少共享数。这可能会为您节省一些昂贵的使用计数原子操作。
std::shared_ptr
的移动操作(如移动构造函数)很便宜,因为它们基本上是"窃取指针">(从源到目标;更准确地说,整个状态控制块从源到目标"被盗",包括引用计数信息)。
相反,std::shared_ptr
调用原子引用计数增加的复制操作(即不仅++RefCount
在整数RefCount
数据成员上,而且例如在 Windows 上调用InterlockedIncrement
),这比仅仅窃取指针/状态更昂贵。
因此,详细分析此案例的参考计数动态:
// shared_ptr<CompilerInvocation> sp;
compilerInstance.setInvocation(sp);
如果按值传递sp
,然后在CompilerInstance::setInvocation
方法中获取副本,则具有:
- 输入方法时,
shared_ptr
参数是复制构造的:ref 计数原子增量。 - 在方法的主体中,将
shared_ptr
参数复制到数据成员中:ref 计数原子增量。 - 退出方法时,
shared_ptr
参数被破坏:ref 计数原子递减。
您有两个原子增量和一个原子递减,总共有三个原子操作。
相反,如果您按值传递shared_ptr
参数,然后在方法内部传递std::move
(如在 Clang 代码中正确完成的那样),则具有:
- 输入方法时,
shared_ptr
参数是复制构造的:ref 计数原子增量。 - 在方法的主体中,您将
shared_ptr
参数std::move
到数据成员中:ref 计数不会改变!你只是在窃取指针/状态:不涉及昂贵的原子引用计数操作。 - 退出方法时,
shared_ptr
参数被破坏;但由于您在步骤 2 中移动,因此没有什么要破坏的,因为shared_ptr
参数不再指向任何内容。同样,在这种情况下不会发生原子递减。
底线:在这种情况下,您只得到一个引用计数原子增量,即只有一个原子操作。
如您所见,对于复制情况,这比两个原子增量加一个原子递减(总共三个原子操作)要好得多。
在这种情况下使用 std::move 有两个原因。大多数回复都解决了速度问题,但忽略了更清楚地显示代码意图的重要问题。
对于 std::shared_ptr,std::move 明确表示 pointee 所有权的转移,而简单的复制操作会添加额外的所有者。当然,如果原始所有者随后放弃了他们的所有权(例如允许他们的std::shared_ptr被销毁),那么所有权的转让就完成了。
当您使用 std::move 转让所有权时,很明显发生了什么。如果您使用普通副本,则在您验证原始所有者立即放弃所有权之前,预期操作是否为转让并不明显。作为奖励,可以更有效地实现,因为所有权的原子转移可以避免所有者数量增加 1 的临时状态(以及随之而来的引用计数变化)。
复制shared_ptr
涉及复制其内部状态对象指针和更改引用计数。移动它只涉及交换指向内部引用计数器和拥有对象的指针,因此速度更快。
由于这些答案都没有提供实际的基准,我想我会尝试提供一个。 但是,想想我让自己比刚开始的时候更困惑。 我试图提出一个测试,该测试将测量通过值、引用和使用std::move
传递shared_ptr<int>
,对该值执行添加操作并返回结果。 我使用两组测试做了几次(一百万次)。 第一组在shared_ptr<int>
中添加了一个常量值,另一组在 [0, 10] 范围内添加了一个随机值。 我认为常量值加成将是大量优化的候选者,而随机值测试则不会。 这或多或少是我所看到的,但是执行时间的极端差异使我相信这个测试程序的其他因素/问题是导致执行时间差异的因素,而不是移动语义。
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;
}
现场版本在这里
我注意到,对于-O0
和-O3
,常量函数都编译为两组标志的同一程序集,都是相对较短的块。 这让我认为大部分优化来自调用代码,但我在我的业余汇编知识中并没有真正看到这一点。
随机函数编译成相当多的汇编,即使是-O3
,所以随机部分必须主导该例程。
所以最后,真的不知道该怎么做。 请向它投掷飞镖,告诉我我做错了什么,提供一些解释。
至少使用 libstdc++ 你应该在移动和分配方面获得相同的性能,因为operator=
调用std::move
在传入指针上。请参阅:https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr.h#L384
不幸的是,我没有读@yano的安维尔。所以我做了自己的基准测试。可悲的是,没有人试图验证这里的假设。我的结果与亚诺斯相似,从某种意义上说,改进远非数百倍。
在我的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));
}