与C ++ 11中的普通指针相比,智能指针的开销是多少?换句话说,如果我使用智能指针,我的代码会变慢吗?如果是的话,速度会慢多少?
具体来说,我问的是C ++ 11 std::shared_ptr
和std::unique_ptr
。
显然,推下堆栈的东西会更大(至少我认为是这样),因为智能指针也需要存储其内部状态(引用计数等),问题确实是,如果有的话,这会影响我的表现多少?
例如,我从函数而不是普通指针返回一个智能指针:
std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();
或者,例如,当我的一个函数接受智能指针作为参数而不是普通指针时:
void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);
答案 0 :(得分:151)
std::unique_ptr
只有在为其提供一些非平凡的删除器时才会有内存开销。
std::shared_ptr
总是有引用计数器的内存开销,尽管它非常小。
std::unique_ptr
只在构造函数中有时间开销(如果它必须复制提供的删除和/或null初始化指针)和析构函数(以销毁拥有的对象)。
std::shared_ptr
在构造函数中有时间开销(用于创建引用计数器),在析构函数中(用于递减引用计数器并可能销毁对象)和赋值运算符(用于递增引用计数器)。由于std::shared_ptr
的线程安全保证,这些增量/减量是原子的,因此增加了一些开销。
请注意,在解除引用(获取对拥有对象的引用)时,它们都没有时间开销,而此操作似乎是指针最常见的操作。
总之,有一些开销,但它不应该使代码变慢,除非你不断创建和销毁智能指针。答案 1 :(得分:21)
与所有代码性能一样,获取硬信息的唯一可靠方法是衡量和/或检查机器代码。
那就是说,简单的推理说
您可能会在调试版本中遇到一些开销,例如operator->
必须作为函数调用执行,以便您可以进入它(这又是由于通常不支持将类和函数标记为非调试)。
对于shared_ptr
,初始创建可能会产生一些开销,因为这涉及动态分配控制块,动态分配比C ++中的任何其他基本操作慢得多(使用{{ 1}}在可行的情况下,尽量减少开销。
同样对于make_shared
,维持引用计数的开销很小,例如通过值shared_ptr
传递时,shared_ptr
没有这样的开销。
记住第一点时,在测量时,请为调试和发布版本执行此操作。
国际C ++标准化委员会已发布technical report on performance,但这是在2006年,在unique_ptr
和unique_ptr
添加到标准库之前。不过,智能指针在那时仍然是旧帽子,所以报告也考虑了这一点。引用相关部分:
“如果 通过简单的智能指针访问值比访问它要慢得多 通过普通指针,编译器无法有效地处理抽象。在里面 过去,大多数编译器都有重大的抽象处罚和几个当前的编译器 还是这样。但是,至少有两个编译器 据报道有抽象 罚款低于1%,罚款3%,所以 消除这种开销是 在最先进的技术范围内“
据了解,截至2014年初,最流行的编译器已经实现了“最先进的技术”。
答案 2 :(得分:17)
我的回答与其他人不同,我真的很想知道他们是否曾编写过代码。
shared_ptr具有很大的创建开销,因为它的控制块的内存分配(它将ref计数器和指针列表保存到所有弱引用)。它也有巨大的内存开销,因为std :: shared_ptr总是一个2指针元组(一个到对象,一个到控制块)。
如果将shared_pointer作为值参数传递给函数,那么它将比正常调用慢至少10倍,并在代码段中创建大量代码以进行堆栈展开。如果你通过引用传递它,你会得到一个额外的间接,这在性能方面也会更差。
这就是为什么你不应该这样做,除非该功能真正涉及所有权管理。否则使用&#34; shared_ptr.get()&#34;。它的目的不是为了确保在正常的函数调用期间你的对象没有被杀死。
如果你发疯并在编译器中的抽象语法树之类的小对象上使用shared_ptr,或者在任何其他图形结构中的小节点上使用shared_ptr,你会看到巨大的性能下降和巨大的内存增加。我见过一个解析器系统,在C ++ 14上市之后不久,程序员学会正确使用智能指针之前,它就被重写了。重写比旧代码慢了很多。
根据定义,这不是一个银弹,原始指针也不是很糟糕。糟糕的程序员是糟糕的,坏的设计是坏的。谨慎设计,设计时考虑到明确的所有权,并尝试主要在子系统API边界上使用shared_ptr。
如果你想了解更多,你可以看看Nicolai M. Josuttis在C ++中对共享指针的实际价格进行了很好的谈论&#34; https://vimeo.com/131189627
它深入探讨了写入障碍,原子锁等的实现细节和CPU架构。一旦听完,你永远不会谈论这个特性便宜。如果你只想要一个较慢的数据证明,跳过前48分钟,并观察他运行的示例代码运行速度慢180倍(使用-O3编译)在任何地方使用共享指针时。
答案 3 :(得分:11)
换句话说,如果我使用智能指针,我的代码会变慢吗?如果是的话,速度会慢多少?
慢?最有可能的是,除非你使用shared_ptrs创建一个巨大的索引,并且你没有足够的内存来计算机开始起皱,就像一位老太太被远方难以忍受的力量猛烈地撞到了地面。
使代码变慢的原因是搜索速度缓慢,不必要的循环处理,大量数据副本以及对磁盘的大量写入操作(如数百个)。
智能指针的优点都与管理有关。 但是必要的开销是什么?这取决于您的实施。假设您正在迭代3个阶段的数组,每个阶段都有1024个元素的数组。为此过程创建smart_ptr
可能过度,因为一旦完成迭代,您就会知道必须将其删除。因此,您可以通过不使用smart_ptr
...
单个内存泄漏可能会使您的产品及时发生故障(假设您的程序每小时泄漏4兆字节,打破计算机需要几个月的时间,然而,它会破坏,你知道它因为泄漏在那里。
就像说“你的软件保证3个月,然后,给我打电话请服务。”
所以最后这真的是......你能处理这种风险吗?使用原始指针处理数百个不同对象的索引值得失去对内存的控制。
如果答案是肯定的,那么使用原始指针。
如果您甚至不想考虑它,smart_ptr
是一个好的,可行的,非常棒的解决方案。
答案 4 :(得分:1)
仅浏览一下[]
运算符,它比原始指针慢约5倍,如以下代码所示,该代码是使用gcc -lstdc++ -std=c++14 -O0
编译并输出以下结果的:>
malloc []: 414252610
unique [] is: 2062494135
uq get [] is: 238801500
uq.get()[] is: 1505169542
new is: 241049490
我开始学习c ++,我已经想到了这一点:您总是需要知道自己在做什么,并花更多的时间来了解其他人在c ++中所做的事情。
根据@Mohan Kumar的方法,我提供了更多详细信息。 gcc版本为7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1)
,使用-O0
时获得了上述结果,但是,当我使用'-O2'标志时,我得到了:
malloc []: 223
unique [] is: 105586217
uq get [] is: 71129461
uq.get()[] is: 69246502
new is: 9683
然后转移到clang version 3.9.0
,-O0
是:
malloc []: 409765889
unique [] is: 1351714189
uq get [] is: 256090843
uq.get()[] is: 1026846852
new is: 255421307
-O2
是:
malloc []: 150
unique [] is: 124
uq get [] is: 83
uq.get()[] is: 83
new is: 54
c -O2
的结果令人惊讶。
#include <memory>
#include <iostream>
#include <chrono>
#include <thread>
uint32_t n = 100000000;
void t_m(void){
auto a = (char*) malloc(n*sizeof(char));
for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
auto a = std::unique_ptr<char[]>(new char[n]);
for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u2(void){
auto a = std::unique_ptr<char[]>(new char[n]);
auto tmp = a.get();
for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
auto a = std::unique_ptr<char[]>(new char[n]);
for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
auto a = new char[n];
for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
int main(){
auto start = std::chrono::high_resolution_clock::now();
t_m();
auto end1 = std::chrono::high_resolution_clock::now();
t_u();
auto end2 = std::chrono::high_resolution_clock::now();
t_u2();
auto end3 = std::chrono::high_resolution_clock::now();
t_u3();
auto end4 = std::chrono::high_resolution_clock::now();
t_new();
auto end5 = std::chrono::high_resolution_clock::now();
std::cout << "malloc []: " << (end1 - start).count() << std::endl;
std::cout << "unique [] is: " << (end2 - end1).count() << std::endl;
std::cout << "uq get [] is: " << (end3 - end2).count() << std::endl;
std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
std::cout << "new is: " << (end5 - end4).count() << std::endl;
}
答案 5 :(得分:1)
Chandler Carruth 在 2019 年的 Cppcon 演讲中对 unique_ptr
有一些令人惊讶的“发现”。 (Youtube)。我也无法解释清楚。
我希望我理解了两个要点:
unique_ptr
的代码将(通常是错误的)无法处理在传递指针时没有传递 owership 的情况。将其重写为使用 unique_ptr
会增加该处理,并且会产生一些开销。unique_ptr
仍然是 C++ 对象,对象在调用函数时会在堆栈上传递,而指针可以在寄存器中传递。