尽管您使用的是较小的连续块(更像是4千字节而不是4兆字节),但我围绕您描述的数据结构的类型介绍了代码库(ECS引擎)中最核心的部分。
它使用双重空闲列表来实现恒定时间的插入和删除,其中一个空闲列表用于准备插入的空闲块(不完整的块),而块内的子空闲列表用于该块中的索引。准备插入时回收。
我将介绍这种结构的优缺点。让我们从一些缺点开始,因为它们有很多:
缺点
- 向此结构中插入数亿个元素所需的时间比
std::vector
(一个连续的结构)要长约4倍。我在微优化方面相当不错,但是从概念上讲,还有更多工作要做,因为通常情况下,首先要检查块空闲列表顶部的空闲块,然后访问该块并从该块的弹出弹出索引空闲列表,将元素写入空闲位置,然后检查块是否已满,如果是,则从块空闲列表中弹出该块。它仍然是一个固定时间的操作,但是常数要比推回更大std::vector
。
- 给定额外的索引编制算法和额外的间接层,使用随机访问模式访问元素所需的时间大约是原来的两倍。
- 顺序访问不能有效地映射到迭代器设计,因为迭代器每次递增时都必须执行其他分支。
- 它有一点内存开销,通常每个元素大约1位。每个元素1位听起来可能并不多,但是如果使用它存储一百万个16位整数,则比完全紧凑的数组多使用6.25%的内存。但是,在实践中,这往往比使用较少的内存要少得多,
std::vector
除非您压缩vector
来消除它保留的多余容量。另外,我通常不使用它来存储此类小元素。
优点
- 使用
for_each
函数的顺序访问需要对块中的元素进行回调处理,该函数几乎可以与顺序访问的速度相媲美std::vector
(仅相差10%),因此在对性能要求最为严格的用例中,它的效率不会降低很多(在ECS引擎中花费的大部分时间是在顺序访问中)。
- 当块完全变空时,它可以通过结构分配块从中间进行恒定时间的删除。因此,通常可以确保数据结构不会使用过多的内存。
- 它不会使未直接从容器中删除的元素的索引无效,因为它只是使用自由列表方法在随后的插入操作中留下了空位,从而留下了空位。
- 即使这种结构包含大量的元素,您也不必担心内存不足,因为它只需要小的连续块,这对于操作系统查找大量连续未使用的块不会构成挑战。页面。
- 由于操作通常局限于单个块,因此它很适合并发性和线程安全性,而无需锁定整个结构。
现在,对我而言,最大的优点之一就是制作这种数据结构的不可变版本变得微不足道,如下所示:
从那时起,这就打开了各种各样的门,可以编写更多没有副作用的函数,这使得实现异常安全,线程安全等变得更加容易。不变性是我发现可以轻松实现的一种东西事后看来,这种数据结构是偶然的,但可以说它最终具有的最大好处之一是,它使维护代码库变得更加容易。
不连续的数组没有缓存局部性,这会导致性能下降。但是,在4M的块大小下,似乎将有足够的位置进行良好的缓存。
在这种大小的块上,您不必担心参考位置,更不用说4 KB的块了。缓存行通常只有64个字节。如果要减少高速缓存未命中,则只需集中精力正确对齐这些块,并在可能的情况下采用更多顺序访问模式即可。
将随机存取存储器模式转换为顺序模式的一种非常快速的方法是使用位集。假设您有大量的索引,并且它们的顺序是随机的。您可以浏览它们并在位集中标记位。然后,您可以遍历位集并检查哪些字节为非零字节,一次检查64位。一旦遇到一组至少设置了一位的64位,就可以使用FFS指令快速确定设置了哪些位。这些位告诉您应该访问哪些索引,除了现在您可以按顺序对索引进行排序。
这有一些开销,但在某些情况下可能是值得的交换,尤其是如果您要遍历这些索引很多次时。
访问一个项目不是那么简单,存在一个间接的附加级别。这会优化吗?会导致缓存问题吗?
不,无法对其进行优化。至少,使用这种结构,随机访问将始终花费更多。尽管由于倾向于使用指向块的指针的数组来获得较高的时间局部性,所以它通常不会增加高速缓存的丢失率,尤其是在常见情况下执行路径使用顺序访问模式的情况下。
由于达到4M限制后线性增长,因此您可以分配比平时更多的分配(例如,对于1GB内存,最多可以分配250个分配)。4M之后不会复制任何额外的内存,但是我不确定额外的分配是否比复制大块内存更昂贵。
在实践中,由于这种情况很少见,因此复制通常会更快,只会发生诸如log(N)/log(2)
总时间之类的事情,而同时简化了这种非常便宜的常见情况,在这种情况下,您只需在数组变满之前需要多次写入元素,然后需要重新分配它。因此,通常情况下,使用这种类型的结构不会获得更快的插入速度,因为即使不需要处理重新分配大型数组的昂贵情况,普通情况下的工作也会更加昂贵。
尽管有很多弊端,但这种结构对我的主要吸引力在于减少了内存使用,不必担心OOM,能够存储不会失效的索引和指针,并发性和不变性。拥有一个数据结构很高兴,您可以在其中不断地插入和删除内容,同时它可以为您自己清理,并且不会使结构中的指针和索引无效。