关于unique_ptr的性能

About unique_ptr performances

本文关键字:性能 ptr unique 关于      更新时间:2023-10-16

我经常读到unique_ptr在大多数情况下比shared_ptr更受欢迎,因为unique_ptr是不可复制的,并且具有移动语义;由于复制和引用计数,Shared_ptr会增加开销;

但是当我在某些情况下测试unique_ptr时,它看起来明显比对应的

慢(在访问中)

例如,在gcc 4.5下:

edit: print方法实际上不打印任何东西

#include <iostream>
#include <string>
#include <memory>
#include <chrono>
#include <vector>
class Print{
public:
void print(){}
};
void test()
{
 typedef vector<shared_ptr<Print>> sh_vec;
 typedef vector<unique_ptr<Print>> u_vec;
 sh_vec shvec;
 u_vec  uvec;
 //can't use initializer_list with unique_ptr
 for (int var = 0; var < 100; ++var) {
    shared_ptr<Print> p(new Print());
    shvec.push_back(p);
    unique_ptr<Print> p1(new Print());
    uvec.push_back(move(p1));
  }
 //-------------test shared_ptr-------------------------
 auto time_sh_1 = std::chrono::system_clock::now();
 for (auto var = 0; var < 1000; ++var) 
 {
   for(auto it = shvec.begin(), end = shvec.end(); it!= end; ++it)
   {
     (*it)->print();
   }
 }
 auto time_sh_2 = std::chrono::system_clock::now();
 cout <<"test shared_ptr : "<< (time_sh_2 - time_sh_1).count() << " microseconds." << endl;
 //-------------test unique_ptr-------------------------
 auto time_u_1 = std::chrono::system_clock::now();
 for (auto var = 0; var < 1000; ++var) 
 {
   for(auto it = uvec.begin(), end = uvec.end(); it!= end; ++it)
   {
     (*it)->print();
   }
 }
 auto time_u_2 = std::chrono::system_clock::now();
 cout <<"test unique_ptr : "<< (time_u_2 - time_u_1).count() << " microseconds." << endl;
}

我平均得到(g++ - 0):

  • shared_ptr: 1480微秒
  • unique_ptr: 3350微秒

差异从何而来?

2014年1月1日更新

我知道这个问题很老了,但是结果在g++ 4.7.0和libstdc++ 4.7上仍然有效。所以,我想找出原因。

您在这里基准测试的是使用- 0解引用性能,并且查看unique_ptrshared_ptr的实现,您的结果实际上是正确的。

unique_ptr将指针和删除器存储在::std::tuple中,而shared_ptr直接存储裸指针句柄。因此,当您解引用指针时(使用*,->或get),您将在unique_ptr中额外调用::std::get<0>()。相反,shared_ptr直接返回指针。在gcc-4.7上,即使经过优化和内联,::std::get<0>()也比直接指针。慢一点。在优化和内联时,gcc-4.8.1完全省略了::std::get<0>()的开销。在我的机器上,当用-O3编译时,编译器生成完全相同的汇编代码,这意味着它们字面上是相同的。总而言之,使用当前的实现, shared_ptr在创建、移动、复制和引用计数方面比慢,但是在解引用*方面和一样快。

说明: print()在问题中为空,编译器在优化时会忽略循环。因此,我稍微更改了代码以正确观察优化结果:

#include <iostream>
#include <string>
#include <memory>
#include <chrono>
#include <vector>
using namespace std;
class Print {
 public:
  void print() { i++; }
  int i{ 0 };
};
void test() {
  typedef vector<shared_ptr<Print>> sh_vec;
  typedef vector<unique_ptr<Print>> u_vec;
  sh_vec shvec;
  u_vec uvec;
  // can't use initializer_list with unique_ptr
  for (int var = 0; var < 100; ++var) {
    shvec.push_back(make_shared<Print>());
    uvec.emplace_back(new Print());
  }
  //-------------test shared_ptr-------------------------
  auto time_sh_1 = std::chrono::system_clock::now();
  for (auto var = 0; var < 1000; ++var) {
    for (auto it = shvec.begin(), end = shvec.end(); it != end; ++it) {
      (*it)->print();
    }
  }
  auto time_sh_2 = std::chrono::system_clock::now();
  cout << "test shared_ptr : " << (time_sh_2 - time_sh_1).count()
       << " microseconds." << endl;
  //-------------test unique_ptr-------------------------
  auto time_u_1 = std::chrono::system_clock::now();
  for (auto var = 0; var < 1000; ++var) {
    for (auto it = uvec.begin(), end = uvec.end(); it != end; ++it) {
      (*it)->print();
    }
  }
  auto time_u_2 = std::chrono::system_clock::now();
  cout << "test unique_ptr : " << (time_u_2 - time_u_1).count()
       << " microseconds." << endl;
}
int main() { test(); }

注意:这不是一个基本问题,可以通过在当前libstdc++实现中放弃使用::std::元组来轻松解决。

您在限时块中所做的只是访问它们。这不会涉及任何额外的开销。增加的时间可能来自控制台输出滚动。您永远不能在定时基准测试中执行I/O操作。

如果你想测试ref计数的开销,那么实际上做一些ref计数。如果您从不对shared_ptr 进行变异,那么shared_ptr的构造、销毁、赋值和其他变异操作所增加的时间将如何影响您的时间呢?

编辑:如果没有I/O,那么编译器优化在哪里?他们应该把整个地方都炸了。就连ideone也把货扔了。

这里没有测试任何有用的东西。

你在说什么: copy

测试内容:迭代

如果你想测试拷贝,你实际上需要执行一个拷贝。当涉及到读取时,这两个智能指针应该具有相似的性能,因为好的shared_ptr实现将保留指向对象的本地副本。

编辑:

关于新元素:

一般来说,在使用调试代码时,甚至不值得讨论速度。如果您关心性能,您将使用发布代码(通常是-O2),因此这是应该衡量的,因为调试代码和发布代码之间可能存在显著差异。最值得注意的是,内联模板代码可以大大减少执行时间。

关于基准:

  • 我想添加另一轮措施:裸指针。正常情况下,unique_ptr和裸指针应该有相同的性能,值得检查,在调试模式下不一定是真的。
  • 您可能想要"交错"执行两个批次,或者如果不能,则在几次运行中取每个批次的平均值。事实上,如果计算机在基准测试结束时变慢,只有unique_ptr批会受到影响,这会干扰测量。

你可能有兴趣从《Neil: The Joy of benchmark》中了解更多,这不是一个权威的指南,但它很有趣。特别是关于强制副作用以避免去除死代码的部分;)

另外,要注意如何测量。你的时钟的分辨率可能没有看起来那么精确。例如,如果时钟每15秒才刷新一次,那么任何15秒左右的测量都是可疑的。在度量发布代码时,这可能是一个问题(您可能需要在循环中添加几个回合)。