【问题标题】:The cost of passing by shared_ptr通过 shared_ptr 的成本
【发布时间】:2011-01-30 22:30:17
【问题描述】:

我在整个应用程序中广泛使用 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 配置文件的 sn-p:

每个样本计为 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 不应该用于经常调用的函数。

任何意见将不胜感激。感谢阅读。

-阿尔泰姆

更新:在更改了一些函数以接受const Dataset&amp; 后,新的配置文件如下所示:

每个样本计为 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&)

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

【问题讨论】:

  • 相关问题:在某些平台(例如旧版 ARM)上,引用计数需要锁定互斥体。这会使共享指针在实时上下文中不可用(通过引用除外)。
  • 您是如何获得这些漂亮的基准数据的?
  • @Daniel:这只是来自gprofflat profile

标签: c++ performance shared-ptr


【解决方案1】:

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

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

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

  • 在整个应用程序中大量使用 shared_ptr 时,按值传递将占用大量时间(我已经看到它超过 50%)。
  • 当参数不能为空时,使用const T&amp; 而不是const shared_ptr&lt;T const&gt;&amp;
  • 当存在性能问题时,使用 const shared_ptr&lt;T const&gt;&amp; 比使用 const T* 更安全。

【讨论】:

  • 这就是我所做的 - 这里存在安全问题,但它们是极端情况。我宁愿有一个稍微慢一点的正确应用程序而不是一个死快的应用程序。您的测试应用是测试平台还是真实代码?
  • @pm100:请注意,我传递的是 const 引用,而不仅仅是引用,这可以防止大多数安全问题,同时提供显着的性能提升。当替代方法是传递 shared_ptr 的原始指针内容时,此方法更安全。
  • 这是我的原创设计,但 Fred 链接的相关问题让我有点犹豫要不要坚持下去。虽然,对 f() 和 g() 的调用相当紧张,我可以保证它们不会重置 pds(直接或间接)。所以,我想问题是哪个更有效 void f( const shared_ptr& pds ) {...} 或 void f( const Dataset& pds ) {...}取消引用发生:在 f() 内部或外部。
  • @Artem:如果您的方法要求pds 为非空,则绝对首选const Dataset&amp;。如果 null 是有效值,则只需传递 shared_ptr
  • Herb Sutter recommends 只是通过原始指针/引用(43 分钟),除非你想做一些类似存储它的事情。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • 内存局部性不会减少函数调用的次数,但通常仍会加快速度。
    【解决方案4】:

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

    考虑一下 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) 吗?我同意在循环外缓存取消引用的值会提高性能。感谢您的建议。
    • 哈哈,是的,我做到了。编辑应该反映这一点。
    【解决方案5】:

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

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

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

    【讨论】:

    • 不,你并不安全。如果您有 2 个不同的 shared_ptr 池指向同一个物理对象,则一个池将在另一个池之前将其删除...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-13
    • 1970-01-01
    • 1970-01-01
    • 2021-12-25
    • 2017-03-30
    • 1970-01-01
    • 2021-12-31
    相关资源
    最近更新 更多