将链表嵌入数据结构有什么好处?

时间:2017-03-04 17:35:12

标签: c++ optimization intrusive-containers

在阅读FreeBSD中的内核数据结构时,我偶然发现了MBufMBuf包含指向MBuf链中下一个MBuf的指针,用于实现链接列表。每个MBuf本身也包含特定于链接列表中该节点的数据。

我更熟悉将容器类型与值类型分开的设计(考虑std::listSystem.Collections.Generic.LinkedList)。我正在努力理解将容器语义嵌入数据类型的价值主张 - 获得了哪些效率?这真的是关于消除节点实例指针存储吗?

3 个答案:

答案 0 :(得分:2)

考虑您有一个指向列表中节点的迭代器/指针。为了获取您需要的数据:

  • 读取指向节点
  • 的数据的指针
  • 取消引用您刚读过的指针并读取实际数据

另一方面,如果列表概念是“嵌入”在您的数据结构中,您可以在单个内存操作中读取您的对象,就像它与节点本身一样。

分隔列表节点及其数据的另一个问题是列表节点本身很小(通常只有2或3个指针)。结果,在存储器中保持这种小结构的存储器开销可能很重要。你知道 - 诸如newmalloc之类的操作实际上消耗的内存比它们分配的更多 - 系统使用它们自己的树结构来跟踪内存的空闲位置和不存在的位置。

在这种情况下,将事物分组到单个分配操作中是有益的。您可以尝试将多个列表节点保存在小包中,或者您可以尝试使用它分配的数据连接每个节点。

使用侵入式指针(与共享指针相比)可以看到类似的策略,或者将对象和智能指针数据打包在一起的std::make_shared

zett42发表评论std::list<T>T与节点数据保持在一起。这实现了我上面解释的单个内存块,但是有一个不同的问题:T不能是多态的。如果您有一个班级A及其派生B,则node<B>不是node<A>的衍生版。如果您努力将B插入std::list<A>,您的对象将会:

  • 在最好的情况下,导致编译错误(没有构造函数A::A(const B&)
  • 在最糟糕的情况下,无声地slice B仅将代表A的部分复制到节点中。

如果要在单个列表中保存多态对象,典型的解决方案实际上是std::list<A*>而不是std::list<A>。但是你最终得到了我上面解释的额外间接。

另一种方法是创建一个侵入式列表(例如boost::intrusive::list),其中节点信息实际上是A对象的一部分。然后每个节点都可以是A的导数而没有问题。

答案 1 :(得分:2)

Intrusive链表的一大优势是您可以创建一个预先存在的对象列表,而无需任何新的分配。要使用std ::指针列表执行此操作,需要进行内存分配。

Boost有一个侵入式列表实现,有正当理由可供使用。 http://www.boost.org/doc/libs/1_63_0/doc/html/intrusive.html

答案 2 :(得分:0)

  

获得了哪些效率?这真的是关于消除节点实例指针存储吗?

我会说缓存丢失更少,然后整体性能更好(即使链接列表通常不是缓存友好的数据结构)。
这样,您不必再按照一个指针在内存中的某处找到您的数据,并将它们放在处理器附近,以用于每个节点。
此外,如果你在一个连续的内存区域构建你的节点并用几个指针管理它们(我们称之为免费列表和使用中的列表,它听起来很熟悉吗?),你可以在性能(至少只要列表中不包含很多项,否则风险就是在内存中来回跳转)。在这种情况下,在和删除中有恒定的时间(当然,除非你必须先搜索列表中的节点才能插入特定的位置),这是另一个优点。