经过shared_ptr的代价是什么?

66

我在我的应用程序中广泛使用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对象被创建和销毁。以下是最近一次运行的flat 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

更新: 将一些函数更改为接受 const Dataset&后,新的配置文件如下:

每个样本计为0.01秒。
  % 累计  自身             自身     总数
时间    秒钟    秒钟      调用    s / call  s / call名称
 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&)

我对析构函数调用次数比复制构造函数调用次数少有些困惑,但总体上,我对相关运行时间的减少非常满意。感谢大家的建议。


相关问题:https://dev59.com/EHRC5IYBdhLWcg3wVvnL - Fred Larson
1
相关问题:在某些平台上(例如旧版ARM),引用计数需要锁定互斥量。这可能会使共享指针在实时环境中无法使用(除非通过引用)。 - Mike Seymour
你是怎么得到这些漂亮的基准数据的? - Daniel
1
@Daniel:这只是gprof平面分析报告 - Artem Sokolov
5个回答

61

始终通过const引用传递您的shared_ptr

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*更安全。

1
这就是我所做的 - 这里有一些安全问题,但它们只是极端情况。我宁愿拥有一个稍微慢一点但正确的应用程序,而不是一个速度快但有缺陷的应用程序。你的测试应用程序是一个测试平台还是真实代码? - pm100
1
@pm100:注意,我传递的是 const 引用,而不仅仅是引用,这可以在提供显著性能提升的同时防止大多数安全问题。当替代方案是传递 shared_ptr 的原始指针内容时,此方法更安全。 - Sam Harwell
这一直是我的原始设计,但Fred提出的相关问题让我有些犹豫是否坚持这个设计。虽然对f()和g()的调用非常紧密,我可以保证它们不会重置pds(直接或间接地)。所以,我想问题是哪个更有效率:void f( const shared_ptr& pds ) {...}还是void f( const Dataset& pds ) {...}看起来区别仅在于解引用发生的位置:在f()内部还是外部。 - Artem Sokolov
4
如果你的方法需要pds不为空,一定要优先选择const Dataset&。只有在空值是有效值时才需要传递shared_ptr - Sam Harwell
6
Herb Sutter建议(在43分钟处)只是通过原始指针/引用进行传递,除非您想要像存储它这样的操作。 - David C. Bishop
显示剩余2条评论

15

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


4
如果您没有使用make_shared,那么能否试试呢?通过将引用计数和对象定位在同一内存区域,您可能会看到与缓存一致性相关的性能提升。无论如何都值得一试。

1
内存局部性并不会减少函数调用的次数,但通常可以加快程序的运行速度。 - Matthieu M.

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也需要访问比绝对必要更多的内存。


你是不是想写 f(ds) 和 g(ds)?我同意在循环外缓存解引用值会提高性能。感谢您的建议。 - Artem Sokolov
哈哈,是的,我做了。编辑应该反映出来。 - dash-tom-bang

1

听起来你真的知道你在做什么。你已经对你的应用程序进行了分析,知道哪里使用了循环。你明白只有在不断调用引用计数指针的构造函数时才会变得昂贵。

我唯一能给你的建议是:假设在函数f(t *ptr)内部,如果你调用另一个使用共享指针的函数,并且你执行other(ptr),而other将原始指针转换为共享指针。当第二个共享指针的引用计数达到0时,你实际上已经删除了你的对象...即使你并不想这样做。你说你经常使用引用计数指针,所以你必须注意这种边角情况。

编辑: 你可以将析构函数设置为私有,并且只有共享指针类的友元才能调用,这样析构函数只能被共享指针调用,这样就安全了。 无法防止共享指针多次删除。根据Mat的评论。


1
不,你并不更安全。如果你有两个指向同一个物理对象的不同shared_ptr池,其中一个池会在另一个之前删除它... - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接