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

hgqdbh6s  于 2023-03-20  发布在  其他
关注(0)|答案(7)|浏览(265)

在C11中,智能指针的开销与普通指针相比是多少?换句话说,如果我使用智能指针,我的代码会变慢吗?如果是,会变慢多少?
具体来说,我问的是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);
oipij1gg

oipij1gg1#

只有在为std::unique_ptr提供一些重要的删除器时,它才会有内存开销。
std::shared_ptr总是有引用计数器的内存开销,尽管它非常小。
std::unique_ptr仅在构造函数(如果它必须复制提供的删除器和/或空初始化指针)和析构函数(销毁所拥有的对象)期间具有时间开销。
std::shared_ptr在构造函数(创建引用计数器)、析构函数(递减引用计数器并可能销毁对象)和赋值运算符(递增引用计数器)中有时间开销。由于std::shared_ptr的线程安全保证,这些递增/递减是原子的,因此增加了一些开销。
注意,它们都没有解引用(获取对所拥有对象的引用)的时间开销,而这种操作似乎是指针最常见的。
总而言之,这会产生一些开销,但不会使代码变慢,除非您不断地创建和销毁智能指针。

fkaflof6

fkaflof62#

我的回答与其他人不同,我真的想知道他们是否曾经剖析过代码。
shared_ptr在创建时有很大的开销,因为它为控制块分配了内存(它保留了ref计数器和指向所有弱引用的指针列表)。它也有很大的内存开销,因为std::shared_ptr总是一个2指针元组(一个指向对象,一个指向控制块)。
如果你把shared_pointer作为一个值参数传递给一个函数,那么它的速度会比普通调用慢至少10倍,并且会在代码段中创建大量代码来进行堆栈展开;如果你通过引用传递它,你会得到一个额外的间接寻址,这在性能方面也会相当糟糕。
这就是为什么你不应该这样做,除非函数真的涉及到所有权管理。否则使用“shared_ptr.get()"。它的设计不是为了确保你的对象在正常的函数调用中不会被杀死。
如果你疯狂地在编译器中的抽象语法树或任何其他图形结构的小节点上使用shared_ptr,你会看到性能的大幅下降和内存的大幅增加。我见过一个解析器系统,它在C14上市后不久,在程序员学会正确使用智能指针之前被重写。重写比旧代码慢了一个数量级。
它不是银的,原始指针也不是坏的定义。坏的程序员是坏的,坏的设计是坏的。设计要小心,设计时要有明确的所有权,尽量在子系统API边界上使用shared_ptr。
如果你想了解更多,你可以观看Nicolai M. Josuttis关于“C
中共享指针的真实的价格”https://vimeo.com/131189627的精彩演讲
它深入到实现细节和CPU架构的写屏障,原子锁等。一旦听你永远不会谈论这个功能是廉价的。如果你只是想要一个证明的幅度慢,跳过前48分钟,看他运行的示例代码运行高达180倍慢(编译与-O3)时,使用共享指针无处不在。
编辑日期:
如果您想了解“std::unique_ptr”,请访问“CppCon 2019:钱德勒·卡鲁斯“没有零成本的抽象”
它只是不正确的,unique_ptr是100%免费的。
离题:
二十多年来,我一直试图教育人们,使用不抛出的异常不会带来成本损失,在这种情况下,成本损失在于优化器和代码大小。

j8ag8udp

j8ag8udp3#

与所有代码性能一样,获得硬信息的唯一真正可靠的方法是度量和/或检查机器代码。
也就是说,简单的推理是

  • 在调试构建中,您可以预期一些开销,因为operator->必须作为函数调用执行,以便您可以单步执行它(这反过来是由于通常缺乏对将类和函数标记为非调试的支持)。
  • 对于shared_ptr,您可以预期在初始创建时会有一些开销,因为这涉及到控制块的动态分配,而动态分配比C++中的任何其他基本操作都要慢得多(尽可能使用make_shared,以最小化开销)。
  • 同样对于shared_ptr,在维护引用计数时也有一些最小的开销,例如当按值传递shared_ptr时,但是对于unique_ptr没有这样的开销。

记住上面的第一点,当您度量时,对调试和发布版本都要这样做。
国际C++标准化委员会已经发布了一个technical report on performance,但这是在2006年,在unique_ptrshared_ptr被添加到标准库之前。尽管如此,智能指针在当时还是过时了,所以报告也考虑到了这一点。引用相关部分:
“如果通过普通智能指针访问一个值比通过普通指针访问它慢得多,则编译器没有效率地处理抽象。在过去,大多数编译器具有显著的抽象损失,并且几个当前的编译器仍然具有显著的抽象损失。然而,据报道至少两个编译器具有低于1%的抽象损失,另一个具有3%的抽象损失,因此消除这种开销完全在现有技术的范围内。
作为一个有根据的猜测,截至2014年初,当今最流行的编译器已经实现了“最先进的水平”。

omtl5h9j

omtl5h9j4#

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

更慢?很可能不会,除非你正在使用shared_ptrs创建一个巨大的索引,而你的内存不足,以至于你的计算机开始起皱,就像一个老太太被一个无法承受的力量从远处坠落到地面。
缓慢的搜索、不必要的循环处理、巨大的数据副本和大量的磁盘写操作(比如数百次)会使代码变慢。
智能指针的优点都与管理有关。**但是开销是必要的吗?**这取决于您的实现。假设您正在迭代一个包含3个阶段的数组,每个阶段包含1024个元素。为这个进程创建一个smart_ptr可能有些过分。因为一旦迭代完成,你就会知道你必须擦除它。所以你可以从不使用smart_ptr中获得额外的内存...
但你真的想这么做吗?
一个内存泄漏可能会使您的产品在时间上出现故障点(假设您的程序每小时泄漏4兆字节,这将需要几个月的时间来破坏一台计算机,然而,它会破坏,你知道这一点,因为泄漏就在那里)。
就像说“你的软件是保证3个月,然后,打电话给我的服务。”
所以最后这真的是一个问题...你能处理这个风险吗?使用一个原始指针来处理你对数百个不同对象的索引是否值得失去对内存的控制。
如果答案是肯定的,那么使用原始指针。
如果您根本不想考虑它,那么smart_ptr是一个很好的、可行的、令人敬畏的解决方案。

bvn4nwqk

bvn4nwqk5#

钱德勒Carruth在他2019年的Cppcon演讲中有几个关于unique_ptr的惊人“发现”。(Youtube)。我无法解释它。
我希望我正确理解了两个要点:

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

6yt4nkrj6#

下面的C++20程序显示了复制或重置shared_ptr的开销:

#include <iostream>
#include <memory>
#include <chrono>
#include <vector>
#include <thread>

using namespace std;
using namespace chrono;

int main()
{
    (void)jthread( []() {} ); // dummy thread to force gcc into MT-mode
    constexpr int64_t ROUNDS = 50'000'000;
    using sp_t = shared_ptr<int>;
    sp_t spOrigin( make_shared<int>( 123 ) );
    vector<sp_t> spDests( 100 );
    auto start = high_resolution_clock::now();
    for( int64_t r = ROUNDS; ; )
    {
        auto iterate = [&]<typename Fn>( Fn fn ) -> bool
        {
            for( sp_t &sp : spDests )
                if( --r > 0 ) [[likely]]
                    fn( sp );
                else
                    return false;
            return true;
        };
        if( !iterate( [&]( sp_t &sp ) { sp = spOrigin; } ) )
            break;
        if( !iterate( [&]( sp_t &sp ) { sp = sp_t(); } ) )
            break;
    }
    double ns = duration_cast<nanoseconds>( high_resolution_clock::now() - start ).count() / (double)ROUNDS;
    cout << ns << endl;
}

我的Zen1系统需要大约7ns来复制或清除一个shared_ptr,这大约是28个时钟周期。我认为这对于复制一个“指针”来说太多了。

oymdgrw7

oymdgrw77#

仅供参考,仅针对[]运算符,它比原始指针慢约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

clang -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;
}

相关问题