如何实现缓存友好的动态二进制树

How to implement a cache friendly dynamic binary tree?

本文关键字:动态 二进制 缓存 何实现 实现      更新时间:2023-10-16

根据包括维基百科在内的几个来源,实现二进制树最常用的两种方法是:

  1. 节点和指针(或引用),其中每个节点显式地包含其子节点
  2. 数组其中子节点的位置由其父节点的索引隐式给定

第二个在内存使用引用位置方面明显优越。但是,如果您希望允许从树中插入删除,这可能会导致树不平衡。这是因为这种设计的内存使用量是树深度的指数函数。

假设您希望支持这样的插入和删除。如何实现树,使树遍历充分利用CPU缓存。

我正在考虑为节点创建一个对象池,并将它们分配到一个数组中。通过这种方式,节点将靠近在一起->因此具有良好的参考位置。

但是,如果节点的大小与缓存行的大小相同,这有意义吗

如果L1行大小为64字节,并且访问std::vector<std::uint8_t>(64)的第一个成员,则可能会在L1缓存中包含向量的全部内容。这意味着您可以非常快速地访问任何元素。但是,如果元素的大小与缓存行大小相同,该怎么办?由于L1、L2和L3高速缓存的高速缓存行可能没有太大的不同,因此引用的局部性在这里似乎没有任何帮助。我错了吗?还能做什么?

除非你正在研究如何改进缓存访问模式的二进制树,否则我觉得这是一个XY问题——你想解决的问题是什么?为什么你认为二叉树是解决你的问题的最佳算法?预期的工作集大小是多少?

如果你正在寻找一个通用的关联存储,有多种缓存友好(其他关键词:"缓存高效"、"缓存不经意")算法,比如Judy数组,PDF对此有广泛的解释。

如果您的工作集大小足够小,并且只需要有序的项目集,那么一个简单的有序数组可能就足够了,这可能会带来另一个性能优势——分支预测。

最后,要找出最适合您的用例的方法,需要尝试并衡量不同的方法。

使用块分配器。

您有一个或几个连续的内存"池",您可以从中分配固定大小的块。它被实现为一个链表。所以分配只是

answer = head, 
head = head->next, 
return answer; 

释放只是

tofree->next = head;
head = tofree;

如果允许多个池,当然需要编写代码来确定池,这会增加一些复杂性,但不会增加太多。它本质上是一个简单的内存分配系统。由于所有池成员在内存中都很接近,所以在小树上可以获得良好的缓存一致性。对于大树,你得聪明一点。