3

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

但是当我在某些情况下测试 unique_ptr 时,它似乎比它的对应物慢得多(在访问中)

例如,在gcc 4.5下:

编辑:打印方法实际上不打印任何东西

#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++ -O0) :

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

差异来自哪里?可以解释吗?

4

3 回答 3

22

2014 年 1 月 1 日更新

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

您在这里进行基准测试的是使用-O0的取消引用性能,并且查看and的实现,您的结果实际上是正确的。unique_ptrshared_ptr

unique_ptr将指针和删除器存储在 a 中::std::tuple,而shared_ptr直接存储裸指针句柄。因此,当您取消引用指针(使用 *、-> 或 get)时,您需要额外调用::std::get<0>()in unique_ptr。相反,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::tuple 轻松解决。

于 2012-10-10T00:00:14.197 回答
12

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

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

编辑:如果没有 I/O,那么编译器优化在哪里?他们应该用核武器摧毁整个事情。甚至ideone也扔掉了很多。

于 2011-11-15T15:07:16.480 回答
3

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

你在说什么:复制

你在测试什么:迭代

如果要测试副本,实际上需要执行副本。两个智能指针在读取时应该具有相似的性能,因为好的shared_ptr实现会保留指向对象的本地副本。

编辑:

关于新元素:

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

关于基准:

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

您可能有兴趣从 Neil:The Joy of Benchmarks中了解更多信息,它不是权威指南,但非常有趣。特别是关于强制副作用以避免死代码删除的部分;)

另外,请注意如何测量。你的时钟的分辨率可能不如它看起来的那么精确。例如,如果时钟仅每 15us 刷新一次,那么 15us 左右的任何测量都是可疑的。测量发布代码时可能会出现问题(您可能需要在循环中添加几圈)。

于 2011-11-15T15:09:39.553 回答