从连续内存中的对象创建链接列表

时间:2013-10-04 18:17:25

标签: c++ performance list caching vector

我想迭代存储在一起的对象(以减少缓存未命中)。我是否正确,我可以通过创建一个向量来实现这一点,以便我的所有对象连续定位,然后使用对X的引用创建链接列表?这样我可以非常快速地插入列表的头部,当我遍历列表元素时,它们彼此之间的距离不会太远,因为它们都来自同一个向量?

3 个答案:

答案 0 :(得分:1)

简短的回答是肯定的。由于连续的内存存储,矢量比链表更适合您的需求。迭代向量并获取其元素通常比链表快得多,因为向量中的项目不会太大。

答案 1 :(得分:0)

您是否需要随机访问存储中的每个项目或顺序访问。内存存储有多大,有多少元素?最长的元素有多大?

有多种方法可以访问您的存储空间,

  • 原始顺序遍历存储
  • 使用指向下一个元素的指针
  • 扩充每个存储元素
  • 使用偏移量(跳过计数)将每个存储元素扩充到下一个元素
  • 创建指向存储
  • 的指针的单独数组(向量)
  • 创建一个单独的数组(向量),其中包含偏移到存储

答案 2 :(得分:0)

在某些情况下,使用std::vector存储链接列表的节点可能是非常有用和有效的策略,例如,您需要能够在常量时间从中间删除元素,仍然需要回收空格,在常量时间向前/中间插入元素,具有遍历顺序匹配插入顺序,保留合理的缓存友好访问模式,并将64位链接的内存使用减半,如下所示:

template <class T>
struct Node
{
     // Stores the memory for the element stored in the node.
     typename std::aligned_storage<sizeof(T), alignof(T)>::type data;

     // Points to previous node in the array or previous
     // free node in the array if the node has been removed.
     // Stores -1 if there is no previous node.
     int32_t prev;

     // Points to next node in the array or next free
     // node in the array if the node has been removed.
     // Stores -1 if there is no next node.
     int32_t next;
};

template <class T>
struct List
{
    // Stores all the nodes contiguously.
    std::vector<Node<T>> nodes;

    // Points to the first node in the list.
    // Stores -1 if the list is empty.
    int32_t head;

    // Points to the first free node in the list.
    // Stores -1 if the free list is empty.
    int32_t free_head;
};

std::vector作为内存分配器

在这种情况下,我们实际上将std::vector转换为节点内存分配器,然后将存储绝对地址的64位指针转换为存储数组的相对索引的32位索引。

enter image description here

然而,你可以在上面的图表中看到这个解决方案的缺点(对不起,如果它有点令人困惑,该图表示擦除和重新插入后会发生什么),如果你开始从中间删除元素并重新插入并重新开始空闲空间,虽然你可以继续以原始的插入顺序遍历元素,但是你开始招致更多的缓存未命中,因为跟随链接可以让你开始在内存中来回zig-zag(不再在完美的顺序访问中遍历数组)图案)。插入到中间时会发生同样的事情(这允许在常量时间内完成,但中间的节点可能会分配到数组的后面,从而降低了引用的局部性)。这可能导致在高速缓存行中加载一个内存区域,只有在所有内存区域仅用于回到同一内存区域并再次加载它之前将其驱逐出去。

优化合格

因此,这些类型的“混合”数组/链表解决方案往往具有降低空间局部性的缺点,您擦除的次数越多,从中间向中间插入元素。缓解这种情况的一种方法偶尔会对列表进行“优化复制/交换”,从而恢复空间局部性并使您回到每个prev链接指向前一个索引的位置。数组,每个next链接指向下一个。

比常规更好

尽管如此,即使没有这些“优化传递”,即使在从中间和重新插入多次删除之后,它仍然会产生更多,更少的缓存未命中,而不是使用通用分配器分配节点的链表。在后一种情况下,节点可能散布在内存中的所有位置,以至于您可能会在访问的每个节点上发生缓存未命中,而当您遇到链接列表的恶名时,在许多使用中效率特别低案例。您还可以使用32位索引(除非您实际需要数十亿个节点)而不是64位计算机上的64位指针,将链接的内存使用减半。

索引链接列表

我使用链接列表很多,但他们总是使用这样的解决方案,将节点存储在连续的数组中(一个连续的缓冲区用于存储所有节点,或者一系列连续的缓冲区,每个缓存存储256个节点,例如) ,并经常使用相对索引而不是绝对指针指向节点。当像这样使用链接列表时,它们在实践中变得更加有效。

内存池

回到32位的日子里,我曾经只是为了这个目的使用内存池,就像符合std::allocator的免费列表一样,但是在64位硬件开始流行之后,指针的大小在内存中加倍了使用和我发现开始使用随机访问数据结构作为类比“内存分配器”和相对32位索引更有用。如果列表中存储的元素只有3个单精度浮点数(12个字节),那么将指针大小减半就不是一个微不足道的差异。我发现最大的实际麻烦就是必须使用索引来处理所有内容并且无法直接获取指针数据,因为如果我们使用std::vector作为我们的pop_back,那么链接的内存使用会加倍类比内存分配器,因为它会在每次重新分配内存时使指针无效。

<强>交换和 - pop_back

请注意,如果您不关心遍历顺序,不关心索引失效,并且不需要能够在常量时间内插入任何地方,这种数据结构就不那么有用了。在这种情况下,更有用的是使用一个向量,您可以将要删除的元素中的元素与最后一个{{1}}进行交换。这种结构的主要优点是可以从列表中的任何位置保留恒定时间,从而可以在列表中的任何位置进行常量时间插入,同时允许您以原始的插入顺序进行遍历,并以合理的缓存友好方式进行遍历。