61

我在整个应用程序中广泛使用 std::tr1::shared_ptr。这包括将对象作为函数参数传入。考虑以下:

class Dataset {...}

void f( shared_ptr< Dataset const > pds ) {...}
void g( shared_ptr< Dataset const > pds ) {...}
...

虽然通过 shared_ptr 传递数据集对象可以保证其存在于 f 和 g 中,但函数可能会被调用数百万次,这会导致创建和销毁大量 shared_ptr 对象。这是最近运行的平面 gprof 配置文件的片段:

每个样本计为 0.01 秒。
  % 累计自我自我总计
 时间 秒 秒 呼叫 s/呼叫 s/呼叫名称
  9.74 295.39 35.12 2451177304 0.00 0.00 std::tr1::__shared_count::__shared_count(std::tr1::__shared_count const&)
  8.03 324.34 28.95 2451252116 0.00 0.00 std::tr1::__shared_count::~__shared_count()

因此,大约 17% 的运行时间花在了 shared_ptr 对象的引用计数上。这是正常的吗?

我的应用程序的很大一部分是单线程的,我正在考虑将一些函数重写为

void f( const Dataset& ds ) {...}

并更换电话

shared_ptr< Dataset > pds( new Dataset(...) );
f( pds );

f( *pds );

在我确定当程序流在 f() 内部时对象不会被破坏的地方。但在我跑去更改一堆函数签名/调用之前,我想知道通过 shared_ptr 传递的典型性能损失是什么。似乎 shared_ptr 不应该用于经常调用的函数。

任何输入将不胜感激。谢谢阅读。

-Artem

更新:将一些函数更改为 acceptconst Dataset&后,新配置文件如下所示:

每个样本计为 0.01 秒。
  % 累计自我自我总计
 时间 秒 秒 呼叫 s/呼叫 s/呼叫名称
  0.15 241.62 0.37 24981902 0.00 0.00 std::tr1::__shared_count::~__shared_count()
  0.12 241.91 0.30 28342376 0.00 0.00 std::tr1::__shared_count::__shared_count(std::tr1::__shared_count const&)

我对析构函数调用的数量小于复制构造函数调用的数量感到有点困惑,但总的来说,我对相关运行时间的减少感到非常满意。感谢大家的建议。

4

5 回答 5

60

始终通过您shared_ptrconst参考:

void f(const shared_ptr<Dataset const>& pds) {...} 
void g(const shared_ptr<Dataset const>& pds) {...} 

编辑:关于其他人提到的安全问题:

  • shared_ptr整个应用程序中大量使用时,按值传递将占用大量时间(我已经看到它花费了 50+%)。
  • 当参数不应为空时使用const T&而不是。const shared_ptr<T const>&
  • 使用const shared_ptr<T const>&const T*性能成为问题时更安全。
于 2010-03-23T18:11:59.357 回答
14

您只需要 shared_ptr 将其传递给保留它以备将来使用的函数/对象。例如,某些类可能会保留 shared_ptr 以在工作线程中使用。对于简单的同步调用,使用普通指针或引用就足够了。shared_ptr 不应该完全替换使用普通指针。

于 2010-03-23T18:13:09.700 回答
4

如果您不使用make_shared,您可以尝试一下吗?通过将引用计数和对象定位在同一内存区域中,您可能会看到与缓存一致性相关的性能提升。总之值得一试。

于 2010-03-23T18:14:12.710 回答
3

在性能关键的应用程序中应该避免任何对象的创建和销毁,尤其是冗余的对象创建和销毁。

考虑一下 shared_ptr 正在做什么。它不仅创建一个新对象并填充它,而且还引用共享状态以增加引用信息,并且对象本身可能完全存在于其他地方,这将是你缓存的噩梦。

大概你需要 shared_ptr (因为如果你可以摆脱一个本地对象,你就不会从堆中分配一个),但你甚至可以“缓存” shared_ptr 取消引用的结果:

void fn(shared_ptr< Dataset > pds)
{
   Dataset& ds = *pds;

   for (i = 0; i < 1000; ++i)
   {
      f(ds);
      g(ds);
   }
}

...因为即使 *pds 也需要比绝对必要的更多内存。

于 2010-03-23T18:13:15.363 回答
1

听起来你真的知道自己在做什么。您已经分析了您的应用程序,并且您确切地知道在哪里使用循环。您知道,只有不断地调用构造函数来引用计数指针才会很昂贵。

我能给你的唯一提示是:假设在函数 f(t *ptr) 内部,如果你调用另一个使用共享指针的函数,并且你执行 other(ptr) 并且 other 生成原始指针的共享指针。当第二个共享指针的引用计数达到 0 时,您已经有效地删除了您的对象....即使您不想这样做。你说你经常使用引用计数指针,所以你必须注意这样的极端情况。

编辑:您可以将析构函数设为私有,并且只是共享指针类的朋友,这样析构函数只能由共享指针调用,那么您就安全了。不会阻止从共享指针中多次删除。根据 Mat 的评论。

于 2010-03-23T18:11:55.953 回答