堆分配如何损害硬件缓存命中率

How can heap allocation hurt hardware cache hit ratio?

本文关键字:缓存 命中率 硬件 何损害 分配      更新时间:2023-10-16

我做了一些测试来研究堆分配和硬件缓存行为之间的关系。经验结果具有启发性,但也可能具有误导性,尤其是在不同平台和复杂/不确定的用例之间。

我感兴趣的有两种情况:批量分配(实现自定义内存池)或后续分配(信任操作系统)。

以下是 C++ 中的两个示例分配测试

//Consequent allocations
for(auto i = 1000000000; i > 0; i--)
    int *ptr = new int(0);
    store_ptr_in_some_container(ptr);
//////////////////////////////////////
//Bulk allocation
int *ptr = new int[1000000000];
distribute_indices_to_owners(ptr, 1000000000);

我的问题是这些:

  • 当我为只读操作迭代所有这些操作时,将如何缓存CPU 中的内存可能会自行分区吗?

  • 尽管有经验结果(批量提高性能明显解决方案),当其他一些相对非常小的人时会发生什么批量分配是否覆盖以前分配的缓存?

  • 两者混合在一起以避免代码膨胀并保持代码可读性是否合理?

  • std::vectorstd::liststd::mapstd::set在这些概念中处于什么位置?

通用堆分配器有一组难以解决的问题。 它需要确保释放的内存可以回收,必须支持任意大小的分配,并强烈避免堆碎片。

这将始终包括每个分配的额外开销,即分配器所需的簿记。 它至少必须存储块的大小,以便在释放分配时可以正确回收它。 几乎总是指向堆段中下一个块的偏移量或指针,分配大小通常大于请求的大小,以避免碎片问题。

这种开销当然会影响缓存效率,当元素很小时,即使你从未使用它,你也会忍不住把它放进 L1 缓存。 当您在一个大口中分配数组时,每个数组元素的开销为零。 而且您有一个硬性保证,即每个元素在内存中都是相邻的,因此按顺序迭代数组的速度将与内存子系统支持的速度一样快。

通用分配器的情况并非如此,对于如此小的分配,开销可能是 100% 到 200%。 并且当程序运行了一段时间并且重新分配了数组元素时,也不能保证顺序访问。 值得注意的是,您的大数组无法支持的操作,因此请注意,不要自动假设分配长时间无法释放的巨型数组一定更好。

所以是的,在这种人为的场景中,你很可能会领先于大阵列。

Scratch std::list 从引用的集合类列表中,它的缓存效率非常差,因为下一个元素通常位于内存中完全随机的位置。 std::vector 是最好的,只是一个引擎盖下的数组。 std::map 通常使用红黑树完成,尽可能合理地完成,但您使用的访问模式当然很重要。与 std::set 相同。