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


101

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

具体来说,我在问C ++ 11 std::shared_ptrstd::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);

8
唯一知道的方法是对代码进行基准测试。
Basile Starynkevitch 2014年

你是什​​么意思 std::unique_ptr还是std::shared_ptr
Stefan 2014年

10
答案是42。(换句话说,谁知道,您需要分析代码并了解硬件上的典型工作量。)
Nim 2014年

您的应用程序需要充分利用智能指针,以使其有意义。
user2672165 2014年

在简单的setter函数中使用shared_ptr的成本非常高,并且会增加100%的开销。
Lothar

Answers:


176

std::unique_ptr 仅在为它提供一些非平凡的删除器时才具有内存开销。

std::shared_ptr 尽管它很小,但始终有用于引用计数器的内存开销。

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

std::shared_ptr在构造函数(用于创建引用计数器),析构函数(用于减小引用计数器并可能破坏对象)和赋值运算符(用于增大引用计数器)中具有时间开销。由于的线程安全保证std::shared_ptr,这些增量/减量是原子的,因此增加了更多开销。

请注意,它们在取消引用(获取对拥有的对象的引用)时都没有时间开销,而对于指针,此操作似乎是最常见的。

总而言之,这会有一些开销,但是除非您不断创建和销毁智能指针,否则它不会使代码变慢。


11
unique_ptr析构函数中没有开销。它的作用与使用原始指针的作用完全相同。
R. Martinho Fernandes 2014年

6
与原始指针本身相比,@ R.MartinhoFernandes确实在析构函数中有时间开销,因为原始指针析构函数不执行任何操作。与原始指针的使用方式相比,它肯定没有开销。
lisyarus 2014年

3
值得一提的是,shared_ptr的部分构造/破坏/分配成本是线程安全造成的

1
另外,默认的构造函数std::unique_ptr呢?如果构造一个std::unique_ptr<int>,内部int*将被初始化为nullptr是否喜欢它。
Martin Drozdik '16年

1
@MartinDrozdik在大多数情况下,您也可以对原始指针进行空初始化,以在以后检查其是否为空或类似的东西。不过,将此添加到答案中,谢谢。
lisyarus

26

与所有代码性能一样,获取硬信息的唯一真正可靠的方法是测量和/或检查机器代码。

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

  • 您可能会期望在调试版本中产生一些开销,因为例如operator->必须作为函数调用执行,以便您可以进入它(这又是由于普遍缺乏将类和函数标记为非调试的支持)。

  • 因为shared_ptr您可以预期初始创建时会产生一些开销,因为这涉及到控制块的动态分配,并且动态分配比C ++中的任何其他基本操作要慢得多(请make_shared在实际可行的情况下使用,以最大程度地减少开销)。

  • 同样,shared_ptr在维护引用计数方面有一些最小的开销,例如,在传递shared_ptr按值时,但没有这样的开销unique_ptr

在进行测量时,请牢记上面的第一点,以进行调试和发布。

国际C ++标准化委员会已经发布了有关性能技术报告,但这是在2006年,在此之前,unique_ptr并将shared_ptr其添加到标准库中。尽管如此,智能指针在那时仍然是旧帽子,因此该报告也对此进行了考虑。引用相关部分:

“如果通过普通智能指针访问值比通过普通指针访问值明显慢,则编译器将无法有效地处理抽象。过去,大多数编译器都有大量的抽象惩罚,而目前的几个编译器仍然有。然而,据报道,至少有两个编译器的抽象惩罚低于1%,另外的惩罚为3%,因此消除这种开销在现有技术水平之内”

明智的猜测是,截至2014年初,当今最受欢迎的编译器已实现了“最先进的”。


您能否在回答中添加一些有关我添加到问题中的案例的详细信息?
Venemo

这可能在10年前或更久以前是正确的,但是今天,检查机器代码并不像上面的人所建议的那样有用。取决于指令流水线化,向量化的方式...以及编译器/处理器最终如何处理推测是多快。更少的代码机器代码并不一定意味着更快的代码。确定性能的唯一方法是分析性能。这可以根据处理器以及每个编译器进行更改。
拜伦

我看到的一个问题是,一旦在服务器中使用了shared_ptrs,shared_ptrs的使用便开始激增,很快shared_ptrs成为默认的内存管理技术。因此,现在您已经重复了1-3%的抽象罚款,这些罚款一遍又一遍。
内森·多罗马尔

我认为,对调试版本进行基准测试是一个完全而浪费的时间
Paul Childs

26

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

shared_ptr的创建开销很大,因为它为控制块分配了内存(保留了ref计数器和指向所有弱引用的指针列表)。因此,它还有巨大的内存开销,而且std :: shared_ptr始终是2指针元组(一个指向对象,一个指向控制块)。

如果将shared_pointer作为值参数传递给函数,则它将比普通调用至少慢10倍,并在代码段中创建大量代码以展开堆栈。如果通过引用传递它,则会获得其他间接访问,这在性能方面也可能会更差。

因此,除非该功能确实涉及所有权管理,否则您不应该这样做。否则,请使用“ shared_ptr.get()”。它不能确保在正常的函数调用期间不会杀死您的对象。

如果您发疯并在诸如编译器中的抽象语法树之类的小对象上或在任何其他图形结构中的小节点上使用shared_ptr,您将看到性能下降和内存增加。我看到了一个解析器系统,该系统在C ++ 14投放市场之后以及程序员学会正确使用智能指针之前不久就被重写了。重写比旧代码慢了一个数量级。

这不是灵丹妙药,根据定义,原始指针也不错。糟糕的程序员是坏人,不良的设计是坏人。谨慎设计,设计时要明确所有权,并尝试主要在子系统API边界上使用shared_ptr。

如果您想了解更多信息,可以观看Nicolai M. Josuttis关于“ C ++中共享指针的实际价格”的精彩演讲,网址为https://vimeo.com/131189627
。锁等。一旦您聆听,您将永远不会谈论此功能便宜。如果只想证明幅度更慢,请跳过前48分钟,看看他运行的示例代码在任何地方使用共享指针时,运行速度都要慢180倍(与-O3编译)。


感谢您的回答!您在哪个平台上进行简介?您可以用一些数据备份您的索赔吗?
Venemo

我没有可显示的数字,但您可以在Nico Josuttis的谈话中找到一些内容 vimeo.com/131189627
Lothar

6
听说过std::make_shared()吗?此外,我发现公然滥用的示威行为很无聊……
Deduplicator

2
如果将控制块分配在对象的前面,则“ make_shared”所能做的所有事情都会使您免受额外分配的影响,并为您提供更多的缓存局部性。当您传递指针时,它根本无济于事。这不是问题的根源。
Lothar

14

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

慢点?很有可能不是这样,除非您使用shared_ptrs创建一个巨大的索引,并且您没有足够的内存使计算机开始起皱,就像一个老太太被远方的无法承受的力量猛烈地摔倒在地。

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

智能指针的优点都与管理有关。但是开销是否必要?这取决于您的实现。假设您要遍历3个阶段的数组,每个阶段都有1024个元素的数组。smart_ptr为此过程创建一个可能会过大,因为一旦完成迭代,您就会知道必须删除它。因此,您可以通过不使用smart_ptr... 获得更多的内存。

但是您真的要这样做吗?

一次内存泄漏可能会使您的产品出现时间故障(假设您的程序每小时泄漏4兆字节,需要花数月的时间才能破坏计算机,但是,由于存在泄漏,它会崩溃,您知道的) 。

就像说“您的软件有3个月的保修期,然后致电我寻求服务”。

因此,最后实际上是...您可以应对这种风险吗?使用原始指针来处理数百个不同对象的索引确实值得失去对内存的控制。

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

如果您甚至不想考虑它,a smart_ptr都是一个很好的,可行的,很棒的解决方案。


4
好的,但是valgrind可以很好地检查可能的内存泄漏,因此,只要您使用它,就应该是安全的™
graywolf 2014年

@Paladin是的,如果您可以处理自己的记忆,smart_ptr对于大型团队来说真的很有用
Claudiordgz 2014年

3
我使用unique_ptr,它简化了很多事情,但是不喜欢shared_ptr,引用计数也不是非常有效的GC,也不是很完美
Graywolf 2014年

1
@Paladin如果可以封装所有内容,我会尝试使用原始指针。如果这是我将像参数一样在各处传递的东西,那么也许我会考虑使用smart_ptr。我的大部分unique_ptrs都用于大型实现中,例如main方法或run方法
Claudiordgz 2014年

@Lothar我看到你对我在回答中说的一句话作了解释:Thats why you should not do this unless the function is really involved in ownership management...很好的回答,谢谢,反对
Claudiordgz

0

乍一看,仅对[]运算符而言,它比原始指针慢大约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

lang的结果-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;
}

我现在已经测试了代码,使用唯一指针时仅慢10%。
Mohan Kumar

8
永远不要使用基准进行测试-O0或调试代码。输出效率极低。始终至少使用-O2(或-O3由于不进行矢量化而如今使用-O2
phuclv

1
如果您有时间想要喝咖啡休息,请使用-O4来优化链接时间,所有小小的抽象函数都将内联并消失。
Lothar

您应该free在malloc测试中包括一个调用,并且delete[]要对new(或使变量成为a静态)unique_ptr进行调用,因为s是delete[]在其析构函数的内部进行调用的。
RnMss
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.