【问题标题】:Intrusive lists侵入性列表
【发布时间】:2010-07-29 09:44:08
【问题描述】:

我无法在网上找到太多关于他们的信息。它们是什么以及它们通常在什么时候使用?

谢谢。

【问题讨论】:

    标签: c++ c


    【解决方案1】:

    侵入式列表是指向下一个列表节点的指针存储在与节点数据相同的结构中的列表。这通常是一件坏事,因为它将数据与特定的列表实现联系起来。大多数类库(例如,C++ 标准库)使用非侵入式列表,其中数据对列表(或其他容器)实现一无所知。

    【讨论】:

    • 非侵入式列表意味着“下一个”指针与数据本身位于不同的缓存行上;所以它最终比各种操作的侵入性列表慢 2 倍。这也意味着分配“链接块”并管理它们,这会增加内存消耗和分配/释放开销。根据不同的观点,侵入性列表是好的(为了性能)或坏的(为了方便您不关心最终产品质量差的情况)。
    • @Brendan:这主要是一个 C/Java 问题。 C++ 没有这个问题。 std::list<T> 可以实现为struct _Node { _Node* prev,next; T object }。模板与强大的价值语义相结合,使这些非侵入式列表变得高效。如您所见,没有单独的缓存行或链接块。
    • @MSalters 模板如何提高效率? (它可能更易于维护,但它不会比用 C 实现的东西执行得更快?
    • @nhed:问题在于,在 C 语言中,您必须在通用解决方案和快速解决方案之间做出选择。解决方案是通用的(使用void*)并且您失去了引用的局部性,或者您为每个元素类型重写了列表节点类型。一个很好的例子是 qsortstd::sort,其中 C++ 通常在整数上比 C 高出 600%。是的,C 中的 intsort() 可能与 C++ 一样快,但无法对浮点数进行排序。
    • @nhed std::sort 优势在于内联。 qsort 接收到作用于 void* 的函数的类型擦除指针,而 std::sort (由于模板)知道其参数的类型,更重要的是,在排序中使用的函数(如果它在同一个 TU 中),所以如果它是足够小(而且通常是这样)它可以内联并消除调用开销。
    【解决方案2】:

    我其实很喜欢侵入式模型。

    1. 在内存上更好(没有太多的小分配 指点别的东西)
    2. 它允许您同时拥有一个存在于多个容器中的对象。
    3. 它允许您使用一种搜索模式(哈希)查找元素,然后按字典顺序查找下一个元素
      • 与 #2 相同,但可以使用 boost's multi_index_container 完成,但请注意 multi_index_container 具有某些缺点,这些缺点对于侵入式容器来说不是问题。

    侵入性很好

    ...您只需要知道自己在做什么(对于任何容器都是如此)。

    【讨论】:

    • @Andrew 显然对许多有用的答案(与最佳答案相比)而不是手册页的粘贴。它确实解决了您为什么/何时使用它(问题的第二部分)。尽情享受你的一天
    • 答案实际上是不合逻辑的,不赞成。
    • 这里的第2点似乎完全错误:侵入式列表中的数据只能在一个容器中,因为数据对象只有一个next指针作为“容器”。
    • @ned-batchelder 您可以在侵入式数据节点中拥有任意数量的下一个指针。除了节点的数据成员之外,您还可以拥有 1 个或多个列表节点,每个节点都有自己的下一个/上一个链接。
    • 当然,但是这个列表应该是“侵入式模型相对于非侵入式的优势”。 Intrusive 让一个对象成为多个集合的一部分,但只能是预先计划的特定集合的一部分。非侵入式在允许一个对象同时存在于多个容器中要好得多。
    【解决方案3】:

    令人惊讶的是有多少人完全错误地理解了这一点(例如 Ziezi 的回答)。当事情真的很简单时,人们似乎把事情复杂化了。

    在侵入式链表中,没有明确的“节点”结构/类。相反,“数据”结构/类本身包含对链表中其他数据的下一个和上一个指针/引用。

    例如(侵入式链表节点):

    struct Data { 
       Data *next; 
       Data *prev; 
       int fieldA; 
       char * fieldB; 
       float fieldC; 
    }  
    

    注意 next 和 prev 指针如何与实体的私有数据字段(例如 fieldA)并排并侵入。这“违反”了标准链表强制执行的关注点分离(见下文),但有利于大大减少查找特定节点的链表遍历量以及减少内存分配。

    在侵入式链表中,“链表”本身通常是虚拟的,通常根本不需要创建链表结构/类。

    相反,您可以简单地将头指针存储到某个所有者/管理器中的第一个数据项。 此管理器还包含添加/删除功能以根据需要更新指针。 欲了解更多信息,请参阅https://gameprogrammingpatterns.com/spatial-partition.html

    拥有一对 next/prev 指针表示每个对象只能属于一个列表。但是,您当然可以根据需要添加多对 next/prev 指针(或定义一组 next/prev 指针)以支持多个列表中的对象。

    在非侵入式(即标准)链表中,next/prev 指针是专用“节点”实体的一部分,而实际的 Data 实体只是该节点中的一个字段。

    例如(非侵入式链表节点和数据):

    struct Data { 
       int fieldA; 
       char * fieldB; 
       float fieldC; 
    }  
    
    struct Node { 
       Node *next; 
       Node *prev; 
       Data *data; 
    }  
    

    注意下一个/上一个指针如何不侵入实际的数据实体,并保持关注点的分离。

    更新:

    您可能会看到其他网站(例如 https://www.data-structures-in-practice.com/intrusive-linked-lists/)使用包含 next/prev 指针的“List”结构(实际上是一个节点),并且是“Data”结构/类中的单个侵入字段。

    这确实从数据中隐藏了下一个/上一个指针,但是它需要执行指针运算来访问与列表(节点)关联的实际数据。

    这种方法在我的选项中增加了不必要的复杂性(而不是直接嵌入下一个/上一个字段),只是为了隐藏下一个/上一个指针的可疑目标。如果您需要侵入性列表,请尽可能保持简单。 (此外,在托管内存语言中,无论如何都很难或不可能进行指针运算。)

    【讨论】:

    • 您的Data 类的内存分配数量与Ziezi 的相同。他的T data 成员不是std::unique_ptr<T> data;它与prevnext 连续分配,就像您的示例一样。
    • 当然,我只提到 Ziezi 回答关于侵入性的误解,而不是直接比较内存分配的数量。不过,我对 C++ 内存分配有点生疏,现在只是一个糟糕的托管内存开发人员:),谢谢你的信息。
    • 您可以(并且可能应该)为侵入式列表提供显式的列表节点结构。查看 Linux 内核中的实现。
    【解决方案4】:

    以下是对列表有效的简短说明:

    我。侵入式容器。

    要存储的对象包含允许集成到容器中的附加信息。示例:

    结构节点 { 下一个节点*; // 额外的 节点* 上一个; // 信息 T数据; }

    1。优点:

    • 存储对象本身。

    • 不涉及内存管理。

    • 迭代速度更快。
    • 更好的异常保证。
    • 插入和删除对象的可预测性。 (不需要额外的(不可预测的)内存管理。)
    • 更好的内存局部性。

    2。缺点:

    • 包含容器集成的附加数据。 (每种商店类型都必须适应(修改)容器要求。)
    • 更改存储对象的内容时,请注意可能产生的副作用。(尤其是关联容器。)
    • 插入对象的生命周期管理,独立于容器。
    • 对象可能在从容器中擦除之前被处置,从而导致迭代器失效。
    • 侵入式容器是不可复制和不可分配的。

    二。非侵入式容器(C++ 标准容器)

    对象不“知道”并包含有关要存储的容器的详细信息。示例:

    结构节点 { T数据; }

    1。优点:

    • 不包含有关容器集成的其他信息。
    • 对象的生命周期由容器管理。 (不太复杂。)

    2。缺点:

    • 存储用户传递的值的副本。 (可就地施工。)
    • 一个对象只能属于一个容器。 (或者容器应该存储指向对象的指针。)
    • 存储副本的开销。 (每次分配的簿记。)
    • 不可复制或不可移动的对象不能存储在非侵入式容器中。
    • 无法存储派生对象并仍保持其原始类型。 (切片 - 失去多态性。)

    【讨论】:

    • 不错的死灵。关于您在非侵入性部分中的“缺点”;第 1 点 - 不一定,可以就地施工,即“安置” 第 4 点 - 完全错误
    • @sp2danny 我已经根据您的评论更新了它,谢谢!
    • 在 cons 部分有一个错误:一个对象在 intrusive 中只能属于一个容器,而不是 non-intrusive 情况。下一个/上一个指针只与一个列表相关。
    • -1:这非常令人困惑。正如您所写,侵入式容器的Node 结构与典型的链表节点相同,模板化为T。您应该已经解释过,在侵入式列表中,对象类型继承了列表节点类型,有效地与侵入式容器类型耦合。
    • 不仅非常混乱,而且完全错误。第一个代码示例只是一个经典的非侵入式节点结构。
    【解决方案5】:

    侵入式列表是对象本身就是列表的头部或单元格的列表。根据上下文,它们是好是坏。

    在某些已定义的模块(不安全的类组一起工作)中,绑定类之间的关系可能是最好的方法。它们允许免费直接和全面管理诸如 unicity 之类的共同关系(例如:苹果不会在苹果树中出现两次,这不需要任何密钥,并且苹果不属于两个不同的树),它们是可导航的在两个方向上(直接访问给定苹果的苹果树和给定一些苹果树的苹果)。所有基本操作都是 O(1)(不在某些外部容器中搜索)。

    两个模块之间的侵入性列表非常糟糕。因为它们将被捆绑在一起,而模块的正当性是对代码独立性的管理。

    【讨论】:

    • 关于侵入式链表有时是建模关系的最佳方式的好点。也避免它们在两个模块之间。 +1
    猜你喜欢
    • 2011-04-22
    • 2011-04-13
    • 2020-04-18
    • 2022-06-16
    • 1970-01-01
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多