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

时间:2014-03-10 08:52:30

标签: c++ performance c++11 smart-pointers

与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);

6 个答案:

答案 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_ptrunique_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++ 对象,对象在调用函数时会在堆栈上传递,而指针可以在寄存器中传递。