C ++中的入侵与非侵入式引用计数指针

时间:2010-03-21 09:14:56

标签: c++

过去几年,我普遍接受了

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

入侵智能指针是要走的路

-

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

  1. 我只使用智能指针(所以没有Foo *,只有Ptr)
  2. 我开始为每个类构建自定义分配器。 (所以Foo会使运营商重新加载)。
  3. 现在,如果Foo有一个所有Ptr的列表(因为很容易使用非侵入式智能指针)。
  4. 然后,我可以避免内存碎片问题,因为类Foo移动对象(并且只是更新相应的Ptr)。
  5. 这个Foo在非侵入式智能指针中移动物体比侵入式智能指针更容易的唯一原因是:

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

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

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

    有没有人对昂贵的这个额外的间接层进行了很好的研究?

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

4 个答案:

答案 0 :(得分:7)

侵入性或非侵入性指针之间存在几个重要区别:

第二(非侵入性)的最大优势:

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

首先的好处是当你需要获得智能指针时(至少在boost::shared_ptrstd::tr1::shared_ptr

的情况下
  • 您不能在构造函数和析构函数中使用shared_ptr
  • 在类的层次结构中使用shared_from是非常重要的。

答案 1 :(得分:7)

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

有很多方法可以不共享所有权。 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。结论

虽然我同意Intrusive Pointers可能更快,因为正在进行更少的分配(为shared_ptr分配了3个不同的内存块),但也不太实用。

所以我想指出Boost Intrusive Pointer库,特别是它的介绍:

  

作为一般规则,如果intrusive_ptr是否比shared_ptr更符合您的需求并不明显,请先尝试基于shared_ptr的设计。

答案 2 :(得分:4)

我不知道有关因侵入性非侵入性而导致的额外费用的研究。但我会注意到,非侵入性似乎是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 *,所以裸指针仍然可以泄露出来,通常是非常难以发现的。

答案 3 :(得分:3)

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

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

  

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

根据shared_ptrmake_shared,我很难遇到一个问题,即侵入式智能指针会显着击败shared_ptr。但是,复制和销毁共享指针可能会慢一些。话虽如此,让我补充一点,我很少使用这些智能指针。大多数时候,我所需要的是独一无二的拥有权。