如何实现缓存友好的动态二叉树?
How to implement a cache friendly dynamic binary tree?
根据包括 Wikipedia 在内的多个来源,实现二叉树的两种最常用的方法是:
- 节点和指针(或引用) 其中每个节点 明确地 包含其子节点。
- Array 其中子节点的位置由其父节点的索引隐式给出。
第二个在内存使用和引用位置方面明显更胜一筹。但是,如果您希望允许从树中进行 insertions 和 removals 可能会导致树 [=29] =]不平衡。这是因为此设计的内存使用量是树深度的指数函数。
假设您想支持这样的插入和删除。您如何实现树以便树遍历充分利用 CPU 缓存。
我正在考虑为节点创建一个对象池并将它们分配到一个数组中。这样,节点将靠在一起 -> 因此具有良好的参考位置。
但是如果节点的大小和缓存行的大小一样,这有意义吗?
如果您的 L1 行大小为 64 字节并且您访问 std::vector<std::uint8_t>(64)
的第一个成员,则您的 L1 缓存中可能会包含向量的全部内容。这意味着您可以非常快速地访问任何元素。但是如果元素的大小与缓存行大小相同呢?由于 L1、L2 和 L3 缓存的缓存行是 likely not to be very different,因此引用位置似乎无法在此处提供帮助。我错了吗?还能做什么?
除非您正在研究如何改进缓存访问模式的二叉树,否则我觉得这是一个 XY problem - 您要解决的问题是什么?为什么您认为二叉树是解决您的问题的最佳算法?预期的工作集大小是多少?
如果你正在寻找一个通用的关联存储,有多种cache-friendly(其他关键字:"cache-efficient","cache-oblivious")算法,例如Judy arrays, for which there is an extensive explanation PDF。
如果您的工作集大小足够小,并且您只需要有序的项目集,一个简单的有序数组可能就足够了,这可能会带来另一个性能优势 - branch prediction。
最后,要找出最适合您的 use-case 就是尝试和衡量不同的方法。
使用块分配器。
你有一个或可能是少数连续的内存"pools",你可以从中分配固定大小的块。它是作为链表实现的。所以分配很简单
answer = head,
head = head->next,
return answer;
释放就是
tofree->next = head;
head = tofree;
如果您允许多个池,当然您需要编写代码来确定池,这会增加一点复杂性,但不会增加太多。它本质上是一个简单的内存分配系统。
由于所有池成员在内存中都靠近在一起,因此您可以在小树上获得良好的缓存一致性。对于大树,您必须更聪明一些。
根据包括 Wikipedia 在内的多个来源,实现二叉树的两种最常用的方法是:
- 节点和指针(或引用) 其中每个节点 明确地 包含其子节点。
- Array 其中子节点的位置由其父节点的索引隐式给出。
第二个在内存使用和引用位置方面明显更胜一筹。但是,如果您希望允许从树中进行 insertions 和 removals 可能会导致树 [=29] =]不平衡。这是因为此设计的内存使用量是树深度的指数函数。
假设您想支持这样的插入和删除。您如何实现树以便树遍历充分利用 CPU 缓存。
我正在考虑为节点创建一个对象池并将它们分配到一个数组中。这样,节点将靠在一起 -> 因此具有良好的参考位置。
但是如果节点的大小和缓存行的大小一样,这有意义吗?
如果您的 L1 行大小为 64 字节并且您访问 std::vector<std::uint8_t>(64)
的第一个成员,则您的 L1 缓存中可能会包含向量的全部内容。这意味着您可以非常快速地访问任何元素。但是如果元素的大小与缓存行大小相同呢?由于 L1、L2 和 L3 缓存的缓存行是 likely not to be very different,因此引用位置似乎无法在此处提供帮助。我错了吗?还能做什么?
除非您正在研究如何改进缓存访问模式的二叉树,否则我觉得这是一个 XY problem - 您要解决的问题是什么?为什么您认为二叉树是解决您的问题的最佳算法?预期的工作集大小是多少?
如果你正在寻找一个通用的关联存储,有多种cache-friendly(其他关键字:"cache-efficient","cache-oblivious")算法,例如Judy arrays, for which there is an extensive explanation PDF。
如果您的工作集大小足够小,并且您只需要有序的项目集,一个简单的有序数组可能就足够了,这可能会带来另一个性能优势 - branch prediction。
最后,要找出最适合您的 use-case 就是尝试和衡量不同的方法。
使用块分配器。
你有一个或可能是少数连续的内存"pools",你可以从中分配固定大小的块。它是作为链表实现的。所以分配很简单
answer = head,
head = head->next,
return answer;
释放就是
tofree->next = head;
head = tofree;
如果您允许多个池,当然您需要编写代码来确定池,这会增加一点复杂性,但不会增加太多。它本质上是一个简单的内存分配系统。 由于所有池成员在内存中都靠近在一起,因此您可以在小树上获得良好的缓存一致性。对于大树,您必须更聪明一些。