weak_ptr的性能惩罚是什么?

33

我正在设计一个游戏的对象结构,对于这种情况,最自然的组织形式是一棵树。作为智能指针的忠实粉丝,我专门使用 shared_ptr。但是,在这种情况下,树中的子节点需要访问其父节点(例如,地图上的生物需要能够访问地图数据,也就是它们父节点的数据)。

当然,拥有关系是地图拥有它的生物,因此持有它们的 shared pointers。然而,为了从生物内部访问地图数据,我们需要指向父级的指针 -- 智能指针的方法是使用引用,即 weak_ptr

但是,我曾经读到过锁定 weak_ptr 是昂贵的操作 -- 也许现在不是了 -- 但考虑到 weak_ptr 将经常被锁定,我担心这种设计会导致性能差。

因此,问题来了:

锁定 weak_ptr 的性能惩罚是什么?它有多重要?


1
我不确定,但我猜测它的成本应该大致相当于复制构造一个shared_ptr的成本。 - James McNellis
3
@Kornel: 一定会有性能损失。解引用 shared_ptr 应该和解引用原始指针一样快,因为它内部只需要执行这个操作(每个 shared_ptr 对象都有自己的指针副本)。如果在已删除答案中推荐的解决方案适用于您特定的用例,那么它将给您带来更好的性能(我很惊讶答案被删除了)。 - James McNellis
@James,非常感谢你的澄清 - 是啊,我也不知道为什么它被删除了 :/ - Kornel Kisielewicz
1
只需使用一个原始指针指向父级,这既安全又高效。 - Konrad Rudolph
@JamesMcNellis 刚刚发现了这个,写完了这篇文章之后 https://dev59.com/bXnZa4cB1Zd3GeqPtbag#20290701 - Alec Teal
显示剩余3条评论
3个回答

20

以下为从Boost 1.42源代码中获取的内容(位于<boost/shared_ptr/weak_ptr.hpp>第155行):

shared_ptr<T> lock() const // never throws
{
    return shared_ptr<element_type>( *this, boost::detail::sp_nothrow_tag() );
}

因此,James McNellis的评论是正确的;这是复制构造shared_ptr的成本。


所以我们只有一个引用增量操作?其实并不奇怪...但是在weak_ptr的情况下有一些昂贵的东西——你知道是什么吗?我认为相反的操作(从shared_ptr构造weak_ptr)也应该很简单... - Kornel Kisielewicz
@Kornel Kisielewicz:之前的评论已删除——一开始我以为它只说“仅限参考实现”LOL!我对weak_ptr效率论点的猜测是与内置指针相比而非shared_ptr(毕竟你可以有一个内置指针指向与shared_ptr相同的位置 :) ) - Billy ONeal
是的,那也可能是这样。我发布这个问题后的第二个反应是它可能与线程有关,但似乎弱引用和共享引用使用相同的引用计数结构,所以不应该有区别。 - Kornel Kisielewicz
@James -- 所以性能有所下降 -- 因为使用共享指针,我们只需对其进行引用解除,而不是复制。在弱指针的情况下,我们需要进行复制和引用增量才能使用它,然后在超出范围后递减。这就是42纳秒的细节问题 xP - Kornel Kisielewicz
1
看起来这个答案是错误和误导性的:所有可能与性能相关的代码实际上都在shared_ptr构造函数中,该构造函数尝试锁定weak_ptr。换句话说,它不是复制或复制构造shared_ptr。 - Pavel P

12

为了我的项目,我能够通过在任何boost包含之前添加#define BOOST_DISABLE_THREADS 来显着提高性能。这样可以避免weak_ptr::lock的自旋锁/互斥锁开销,在我的项目中是一个主要瓶颈。由于该项目不涉及与boost有关的多线程操作,所以我可以这么做。


11
使用/解引用shared_ptr几乎类似于访问原始指针,锁定weak_ptr与常规指针访问相比是一种性能较重的操作,因为这段代码必须"线程感知"才能在另一个线程触发释放指针所引用的对象时正常工作。最少也需要执行某种交错/原子操作,根据定义,这种操作比常规内存访问慢得多。

通常,了解正在发生的情况的一种方法是检查生成的代码

#include <memory>

class Test
{
public:
    void test();
};

void callFuncShared(std::shared_ptr<Test>& ptr)
{
    if (ptr)
        ptr->test();
}

void callFuncWeak(std::weak_ptr<Test>& ptr)
{
    if (auto p = ptr.lock())
        p->test();
}

void callFuncRaw(Test* ptr)
{
    if (ptr)
        ptr->test();
}

通过 shared_ptr 和裸指针访问是一样的。因为 shared_ptr 是通过引用传递的,所以我们需要加载引用的值,这就是为什么差异仅仅是多了一个额外的加载操作。

callFuncShared:

enter image description here

callFuncWeak:

enter image description here

通过 weak_ptr 调用会产生 10 倍以上的代码,并且最好情况下还必须通过锁定的比较交换来进行,这本身就需要比解引用裸指针或 shared_ptr 更多的 CPU 时间:

enter image description here

只有共享计数器不为零时,才能加载实际对象的指针并使用它(调用对象或创建 shared_ptr)。


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