与 C++ 中的普通指针相比,智能指针的开销有多大?

与 C++ 11中的普通指针相比,智能指针的开销有多大?换句话说,如果我使用智能指针,我的代码会变慢吗? 如果是的话,会慢多少?

具体来说,我要问的是 C++ 11 std::shared_ptrstd::unique_ptr

显然,堆栈下面的内容会更大(至少我是这么认为的) ,因为智能指针也需要存储它的内部状态(引用计数等) ,问题是,这会对我的性能有多大影响,如果有的话?

例如,我从函数返回一个智能指针,而不是普通指针:

std::shared_ptr getValue();
// versus
const Value *getValue();

或者,例如,当我的一个函数接受智能指针作为参数而不是普通指针时:

void setValue(std::shared_ptr val);
// versus
void setValue(const Value *val);
63474 次浏览

换句话说,如果我使用智能指针,我的代码会变慢吗? 如果是的话,会慢多少?

慢一点?最有可能的情况是,除非你正在使用 share _ ptrs 创建一个巨大的索引,并且你没有足够的内存,以至于你的电脑开始起皱,就像一个老太太从远处被一股难以承受的力量垂直地摔到地上。

使您的代码变慢的原因是缓慢的搜索、不必要的循环处理、大量的数据副本以及大量的磁盘写操作(比如数百次)。

智能指针的优点都与管理有关。这取决于您的实现。假设你迭代一个由3个阶段组成的数组,每个阶段有一个由1024个元素组成的数组。为这个过程创建一个 smart_ptr可能有些夸张,因为一旦迭代完成,您就会知道必须删除它。因此,您可以从不使用 smart_ptr获得额外的内存..。

但你真的想这么做吗?

一次内存泄漏就可能导致你的产品在某个时间点出现故障(比如说你的程序每小时泄漏4兆字节,要花几个月的时间才能让一台计算机崩溃,然而,它还是会崩溃,你知道这一点,因为泄漏就在那里)。

就像说“你的软件保修3个月,然后,打电话给我服务。”

所以到最后,这真的是一个问题... 你能处理这种风险吗?使用原始指针处理数百个不同对象的索引是否值得放松对内存的控制。

如果答案是肯定的,那么使用原始指针。

如果你甚至不想考虑它,一个 smart_ptr是一个很好的,可行的,令人敬畏的解决方案。

只有在提供一些非平凡的删除器时,std::unique_ptr才会有内存开销。

std::shared_ptr对于引用计数器总是有内存开销,尽管它非常小。

std::unique_ptr只有在构造函数期间(如果它必须复制提供的删除器和/或 null-初始化指针)和在析构函数期间(销毁所拥有的对象)才有时间开销。

std::shared_ptr在构造函数(创建引用计数器)、析构函数(减少引用计数器并可能销毁对象)和赋值操作符(增加引用计数器)方面有时间开销。由于 std::shared_ptr的线程安全保证,这些增量/减量是原子的,因此增加了一些开销。

请注意,它们在解引用(获取对所有对象的引用)方面都没有时间开销,而这种操作似乎是指针最常见的操作。

总之,存在一些开销,但是除非您不断地创建和销毁智能指针,否则它不应该使代码变慢。

与所有代码性能一样,获得硬信息的唯一真正可靠的方法是使用 量度和/或 视察机器码。

也就是说,简单的推理说明

  • 可以预期在调试构建中会有一些开销,因为例如 operator->必须作为函数调用执行,这样您才能进入它(这是由于通常缺乏对将类和函数标记为非调试的支持)。

  • 对于 shared_ptr,你可以预期在初始创建时会有一些开销,因为这涉及到控制块的动态分配,而且动态分配比 C + + 中的任何其他基本操作都要慢得多(在实际可能的情况下使用 make_shared,以尽量减少开销)。

  • 对于 shared_ptr来说,在维护引用计数方面也有一些最小的开销,例如通过值传递 shared_ptr时,但是对于 unique_ptr来说没有这样的开销。

请记住上面的第一点,在测量时,对于调试和发布版本都要这样做。

国际 C + + 标准化委员会已经发布了 绩效技术报告,但那是在2006年,在 unique_ptrshared_ptr被添加到标准库之前。尽管如此,聪明的指示器在那个时候已经过时了,所以报告也考虑到了这一点。引用相关部分:

(如果) 通过简单的智能指针访问值要比访问值慢得多 通过普通指针,编译器处理抽象的效率很低 过去,大多数编译器都有明显的抽象缺陷,并且有几个当前的编译器 但是,至少有两个编译器 据报道有抽象现象 少于1% 的罚款和另外3% 的罚款,所以 消除这种开销是 完全处于最先进的水平

作为一种有根据的猜测,截至2014年初,目前最流行的编译器已经实现了符合最先进水平的编译。

我的回答与其他人不同,我真的想知道他们是否曾经分析代码。

Share _ ptr 在创建时有很大的开销,因为它为控制块分配了内存(它保留了指向所有弱引用的 ref 计数器和指针列表)。它还有一个巨大的内存开销,因为 std: : share _ ptr 总是一个2指针元组(一个指向对象,一个指向控制块)。

如果你传递一个分享 _ 指针给一个函数作为值参数,那么它会比普通调用慢至少10倍,并且会在代码段中创建大量的代码用于堆栈展开。如果通过引用传递它,就会得到一个额外的间接性,这在性能方面也会相当糟糕。

这就是为什么你不应该这样做,除非该功能是真正涉及到所有权管理。否则使用“ share _ ptr”。得到()”。它的设计目的不是确保对象在正常的函数调用期间不会被终止。

如果你发疯了,在小对象上使用 share _ ptr,比如编译器中的抽象语法树,或者其他图形结构中的小节点,你会看到巨大的性能下降和巨大的内存增加。我见过一个解析器系统,它在 C + + 14上市后不久就被重写了,在程序员学会正确使用智能指针之前就被重写了。重写比旧代码慢一个数量级。

这不是一个银弹和原始指针也不坏的定义。糟糕的程序员是糟糕的,糟糕的设计是糟糕的。谨慎设计,在设计时考虑清楚的所有权,并尝试主要在子系统 API 边界上使用 share _ ptr。

如果你想了解更多,你可以看 Nicolai M. Josuttis 关于“ C + + 中共享指针的真实价格”https://vimeo.com/131189627的精彩演讲
它深入到实现细节和 CPU 架构的写屏障,原子锁等一旦听你永远不会谈论这个功能是廉价的。如果你只是想要一个更慢的数量级的证明,跳过前48分钟,看他运行示例代码,运行高达180倍慢(用 -O3编译) ,当使用共享指针无处不在。

编辑:

如果您询问“ std: : special _ ptr”,请访问这个演讲 “ CppCon 2019: Chandler Carruth”没有零成本的抽象 Https://www.youtube.com/watch?v=rhikrotswcc

这是不正确的,惟一的 _ ptr 是100% 免费的。

旁白:

我试图教育人们这样一种错误观念,即使用未被抛出的异常,在过去20多年里也没有任何成本惩罚。在这种情况下,它在优化器和代码大小中。

仅仅为了一瞥和仅仅为了 []操作符,它比原始指针慢约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

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


钱德勒 · 卡鲁斯在2019年全国政协的讲话中对 unique_ptr有一些令人惊讶的“发现”。(Youtube).我也解释不清楚。

我希望我正确理解了以下两点:

  • 没有 unique_ptr的代码将不能处理在传递指针时没有传递所有权的情况(通常是不正确的)。将其重写为使用 unique_ptr将添加该处理,这会带来一些开销。
  • unique_ptr仍然是一个 C + + 对象,在调用函数时,对象将在堆栈上传递,这与可以在寄存器中传递的指针不同。