我发现链表在网格和图像处理,物理引擎和光线跟踪等性能至关重要的领域中工作时,最有用的情况之一是,使用链表实际上可以提高引用的局部性并减少堆分配,有时甚至可以减少内存使用量。直截了当的选择。
现在看来好像是一个完整的矛盾词,因为链表通常因其相反而臭名昭著,因此链表可以执行所有操作,但是它们具有独特的属性,因为每个链表节点都有固定的大小和对齐要求,我们可以利用该要求来允许它们将被连续存储,并以可变大小的东西无法固定的方式以恒定的时间删除。
结果,让我们来做个比喻,就是存储一个包含一百万个嵌套的可变长度子序列的可变长度序列。一个具体的示例是一个索引网格,该网格存储一百万个多边形(一些三角形,一些四边形,一些五边形,一些六边形等),有时从网格中的任何位置删除多边形,有时重建多边形以将顶点插入现有多边形或删除一个。在这种情况下,如果我们存储一百万个tiny std::vectors
,那么最终将面临每个向量的堆分配以及潜在的爆炸性内存使用。SmallVectors
在一般情况下,一百万个微小对象可能不会遭受那么多的问题,但是然后,未单独进行堆分配的预分配缓冲区仍然可能导致爆炸性的内存使用。
这里的问题是一百万 std::vector
实例将试图存储一百万个可变长度的东西。可变长度的东西倾向于需要堆分配,因为如果它们没有将内容存储在堆中的其他地方,它们将不能非常有效地连续存储并不能以恒定时间(至少在没有非常复杂的分配器的情况下以直接方式)被删除。
相反,如果我们这样做:
struct FaceVertex
{
// Points to next vertex in polygon or -1
// if we're at the end of the polygon.
int next;
...
};
struct Polygon
{
// Points to first vertex in polygon.
int first_vertex;
...
};
struct Mesh
{
// Stores all the face vertices for all polygons.
std::vector<FaceVertex> fvs;
// Stores all the polygons.
std::vector<Polygon> polys;
};
...然后我们大大减少了堆分配和缓存未命中的数量。现在,我们只需要在存储在整个网格中的两个向量之一超过其容量(摊余成本)的情况下,才需要进行堆分配,而不必为访问的每个多边形都要求堆分配和可能的强制高速缓存未命中。尽管从一个顶点到另一个顶点的步幅仍可能导致其缓存未命中的份额,但与每个多边形存储一个单独的动态数组相比,它仍然通常要少得多,因为节点是连续存储的,并且有可能相邻的顶点可能在逐出之前可以访问(特别是考虑到许多多边形将一次添加所有顶点,这使得多边形顶点的绝大部分完美连续)。
这是另一个示例:
...格网单元用于加速粒子与粒子的碰撞,例如每单帧移动1600万个粒子。在该粒子网格示例中,使用链接列表,我们只需更改3个索引就可以将粒子从一个网格单元移动到另一个网格单元。从向量擦除并推回另一个向量可能会更加昂贵,并且会引入更多的堆分配。链表也将单元的内存减少到32位。向量取决于实现方式,可以将其动态数组预先分配给一个空向量,该动态数组可能需要32个字节。如果我们有大约一百万个网格单元,那将是一个很大的差异。
……这是我发现最近最有用的链接列表的地方,我特别发现“索引链接列表”变体很有用,因为32位索引使64位计算机上链接的内存需求减半了,这意味着节点连续存储在数组中。
通常,我还将它们与索引的空闲列表结合使用,以允许在任何地方进行恒定时间的删除和插入:
在这种情况下,next
如果节点已删除,则索引将指向下一个空闲索引;如果节点尚未被删除,则索引将指向下一个使用的索引。
这是我最近发现链表的第一个用例。例如,当我们要存储一百万个可变长度子序列,平均每个子序列有4个元素(但有时要删除元素并将其添加到这些子序列之一)时,链接列表使我们可以存储400万个子序列链接列表节点是连续的,而不是分别单独堆分配的一百万个容器:一个巨大的向量,即不是一百万个小的向量。