【问题标题】:invasive vs non-invasive ref-counted pointers in C++C ++中的侵入式与非侵入式引用计数指针
【发布时间】:2010-03-21 09:14:55
【问题描述】:

在过去的几年里,我普遍接受这一点

如果我要使用引用计数的智能指针

侵入式智能指针是要走的路

--

但是,由于以下原因,我开始喜欢非侵入式智能指针:

  1. 我只使用智能指针(所以周围没有 Foo*,只有 Ptr)
  2. 我开始为每个类构建自定义分配器。 (所以 Foo 会重载 operator new)。
  3. 现在,如果 Foo 有一个所有 Ptr 的列表(使用非侵入式智能指针很容易做到)。
  4. 然后,我可以避免内存碎片问题,因为类 Foo 移动对象(并且只更新相应的 Ptr)。

这个 Foo 在非侵入式智能指针中移动对象比侵入式智能指针更容易的唯一原因是:

在非侵入式智能指针中,只有一个指针指向每个 Foo。

在侵入式智能指针中,我不知道有多少对象指向每个 Foo。

现在,非侵入式智能指针的唯一成本......是双重间接。 [也许这会破坏缓存]。

有没有人好好研究过这个额外的间接层是昂贵的?

编辑:通过智能指针,我可能指的是其他人所说的“共享指针”;整个想法是:有一个引用计数附加到对象,当它达到 0 时,对象会被自动删除

【问题讨论】:

  • 我想你所说的智能指针是指共享指针(这是一种智能指针)。我在 shared_ptr 方面遇到了太多问题(通常是破坏顺序问题,尤其是在线程系统中),因此认为通常不鼓励使用共享指针。到目前为止,自动存储仍然是最好的选择(在合适的情况下),甚至在其他地方,scoped_ptrs 通常是比 shared_ptrs 更好的选择。
  • @Tronic:我编写了自己的侵入式智能指针。我在多线程问题中使用它们。我从来没有遇到过销毁订单问题。这些只在多线程情况下才会出现的销毁顺序问题是什么?
  • 假设您有两个不同的智能指针对象,它们共享一个指针对象。假设有两个线程想要复制每个智能指针(所以你最终得到 4 个)。这里的问题是,即使对象本身从未被触及,引用计数器的访问也必须同步。

标签: c++


【解决方案1】:

侵入式和非侵入式指针有几个重要的区别:

秒的最大优势(无创):

  • 实现对第二个(即shared_ptr/weak_ptr)的弱引用要简单得多。

first 的优点是当您需要在此上获得智能指针时(至少在 boost::shared_ptrstd::tr1::shared_ptr 的情况下)

  • 你不能在构造函数和析构函数中使用shared_ptr
  • 在类的层次结构中有 shared_from this 是非常重要的。

【讨论】:

    【解决方案2】:

    嗯,首先,我要提醒您,共享所有权通常是难以驯服的野兽,并且可能导致非常难以根除错误。

    有很多方法可以不共享所有权。 Factory 方法(使用Boost Pointer Container 自行实现)是我个人最喜欢的方法之一。

    现在,至于引用计数......

    1.侵入式指针

    计数器嵌入在对象本身中,这意味着:

    • 您需要提供对计数器进行加/减的方法,并且您有责任使它们线程安全
    • 计数不存在于对象中,因此没有weak_ptr,因此如果不使用Observer 模式,您的设计中就不能有引用循环...相当复杂

    2。非侵入式指针

    我只会谈论boost::shared_ptrboost::weak_ptr。我最近深入研究了源代码,以精确查看机制,确实比上面的要复杂得多!

    // extract of <boost/shared_ptr.hpp>
    
    template <class T>
    class shared_ptr
    {
      T * px;                     // contained pointer
      boost::detail::shared_count pn;    // reference counter
    };
    
    • 计数的维护已经为您写好并且是线程安全的。
    • 您可以在循环引用的情况下使用weak_ptr
    • 只有构建shared_ptr 对象的人需要了解对象析构函数(参见示例)

    这里有一个小例子来说明这种前向声明的魔力:

     // foofwd.h
     #include <boost/shared_ptr.hpp>
    
     class Foo;
    
     typedef boost::shared_ptr<Foo> foo_ptr;
    
     foo_ptr make_foo();
    
     // foo.h
     #include "foofwd.h"
    
     class Foo { /** **/ };
    
     // foo.cpp
     #include "foo.h"
    
     foo_ptr make_foo() { return foo_ptr(new Foo()); }
    
     // main.cpp
     #include "foofwd.h"
    
     int main(int argc, char* argv[])
     {
       foo_ptr p = make_foo();
     } // p.get() is properly released
    

    有一点模板魔法来授权这个。基本上,计数器对象嵌入了一个disposer*(第三个分配),它允许进行某种类型的擦除。不过真的很有用,因为它确实允许前向声明!

    3.结论

    虽然我同意侵入式指针可能更快,因为分配较少(为 shared_ptr 分配了 3 个不同的内存块),但也不太实用。

    所以我想向您指出Boost Intrusive Pointer 库,尤其是它的介绍:

    一般来说,如果intrusive_ptr 是否比shared_ptr 更适合您的需求并不明显,请先尝试基于shared_ptr 的设计。

    【讨论】:

    • 为什么是三个分配?你不能将删除器嵌入到计数器对象中吗?结合 make_shared 甚至可以将其简化为一个分配:嵌入指针、引用计数器和删除器的单个对象。
    • 我可以,但这不是它的设计方式,可能是因为关注点分离的想法。我同意将整个分配到一个块中可能会更快。
    【解决方案3】:

    我不知道有关于由于非侵入性而不是侵入性而导致的额外费用的研究。但我会注意到,C++ 专家似乎普遍推荐非侵入式。当然,这可能没有任何意义!但是推理非常合理:如果您需要智能指针,那是因为您想要一种更简单的方法来实现对象生命周期管理而不是手动编写它,因此您强调正确性和简单性而不是性能,这始终是一个好主意,直​​到您已经描述了您整个设计的真实模型。

    很可能在简化的测试中,非侵入式的速度是侵入式的两倍,但在实际工作的实际程序中,这种速度差异会消失在噪音中,变得如此微不足道,你无法做到甚至测量它。这是相当普遍的现象;您想象的对性能很重要的事情往往并不重要。

    如果您发现性能瓶颈,有可能(可能?)维护引用计数本身的工作(在这两种方法中)对性能的影响与非侵入性方法中的额外间接性一样大。使用原始指针,语句:

    p1 = p2;
    

    在优化器发挥了它的魔力之后,可能只需要在两个 CPU 寄存器之间移动一个值。但是,如果它们是引用计数智能指针,即使是侵入性的,它就像:

    if (p1 != p2)
    {
        if ((p1 != 0) && (--(p1->count) == 0))
            delete p1;
    
        p1 = p2;
    
        if (p1 != 0)
            p1->count++;
    }
    

    这发生在每个传递给每个函数的智能指针参数上。所以有很多额外的访问潜在的遥远的内存区域,每次都增加和减少计数。并且为了线程安全,增量和减量检查操作必须是互锁/原子的,这会对多核产生严重的负面影响。

    我认为 C++ 的“最佳位置”是那些您不需要像这样管理非常动态的数据结构的情况。相反,您有一个简单的对象所有权分层模式,因此每个对象都有一个明显的单一所有者,并且数据的生命周期倾向于遵循函数调用的生命周期(通常情况下)。然后,您可以让标准容器和函数调用堆栈为您管理一切。这在即将发布的带有右值引用的语言版本中得到了强调,unique_ptr 等都是关于以简单的方式转移对象的单一所有权。如果你真的需要动态的多所有者生命周期管理,那么真正的 GC 会更快并且更容易正确使用,但 C++ 对 GC 来说并不是一个非常幸福的家。

    另一个小问题:不幸的是,“在非侵入式智能指针中,只有一个指向每个 Foo 的指针”是不正确的。在Foo 内部有一个this 指针,它是一个Foo *,因此裸指针仍然可以泄露出来,通常以非常难以发现的方式。

    【讨论】:

    • 我同意 Foo* 可以泄露出去。但是,这是个人项目;只要我遵守纪律,就不会成为问题。
    • boost::shared_ptr 实现的情况下没有额外的间接性:它拥有两个指针,一个指向计数结构,一个指向对象本身。
    • @Matthieu M. - 这与零开销不同。这意味着需要两倍的存储空间,更大的可能需要的对象不适合缓存,等等。尽管我的观点通常是,这在实践中可能不如一个易于维护、正确的程序重要。
    • 事实上,堆上分配了第三个对象:当计数达到0时会释放该对象的结构,因此绝对不是无开销的情况。但是,如果您不想要任何开销,则需要定义的内存所有者。
    【解决方案4】:

    非侵入性引用计数的唯一实际成本 w.r.t.性能是您有时需要为 ref-counter 分配一个额外的分配。据我所知, tr1::shared_ptr 实现不做“双重间接”。我想,如果不让 shared_ptr 直接存储指针,就很难支持转换。一个明智的 shared_ptr 实现将存储两个指针:一个指向对象的指针(无双重间接)和一个指向某个控制结构的指针。

    即使在所有情况下都不需要分配开销。见make_shared。 C++0x 还将提供一个make_shared 函数,它可以一次性分配对象和引用计数器,这与侵入式引用计数替代方案类似。

    [...] 除了方便和风格之外,这样的函数也是异常安全的,而且速度相当快,因为​​它可以为对象及其相应的控制块使用单个分配,从而消除了 shared_ptr 构造开销的很大一部分。这消除了关于 shared_ptr 的主要效率抱怨之一。 [...]

    鉴于shared_ptrmake_shared,我很难想出侵入式智能指针会显着击败shared_ptr 的问题。不过,复制和销毁共享指针可能会慢一些。话虽如此,让我补充一点,我很少使用这类智能指针。大多数时候,我只需要独特的身份。

    【讨论】:

    • 怎么没有双向,假设我有一个指向 Foo 的形状指针。有原始的 Foo*;有一个结构 Ptr_Foo {Foo* ptr;整数计数; } ...我有一个指向 Ptr_Foo 的指针——这不会破坏缓存,还是我错过了什么?
    • @anon - 这个想法是结构将包含 Foo 的实际实例以及计数,而不是指向 Foo 的指针,
    • 正如我所说,没有人阻止您在智能指针中存储两个指针,一个指向您的 Foo 对象,一个指向控制结构。您无需触摸控制结构即可访问对象。
    • 丹尼尔:你是对的,我错了。感谢您的澄清。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-26
    • 1970-01-01
    • 2020-04-21
    • 1970-01-01
    相关资源
    最近更新 更多