如何在树访问器中实现每个节点的缓存

How to do per node caching in a tree visitor

本文关键字:节点 缓存 实现 访问      更新时间:2023-10-16

我有一个应用程序,其中我想计算通过布尔运算(内部节点)组合的原语树(叶节点)的不同表示(网格,体素化,带符号距离函数,…)。

我的第一种方法是为每个不同的表示编写一个带有虚拟getter函数的抽象基类,并将中间结果缓存在各自的节点上,只要它们的子树没有变化(这会刷新它们的缓存)。

然而,我对树结构与每种不同表示的丑陋耦合感到不满意。为了减轻这个问题,我删除了抽象基类,并为每个表示设置了一个访问者。

这巧妙地将树与表示解耦,但给我留下了一个问题,我现在需要在其他地方缓存中间结果,这就是我的问题开始的地方。

TL;博士

如何在树的内部节点缓存(任意许多不同类型的)中间值,而不使树依赖于值类型?

我的方法

需求提供了两个选择:

  • 将数据存储在树中,但带有类型擦除
  • 将数据存储在树外,并以某种方式将其"连接"到节点

第一个让我对一些效率问题感到困惑:我可以很容易地在节点中添加一个boost::any(或类似的东西)的容器,但然后每个访问者都必须搜索整个容器以获取自己的数据。

第二个示例中的分离引入了保持当前树的缓存更新的问题。如果在树中有更改(删除,更改节点),缓存的值必须至少是无效的。我的直觉是使用哈希函数和unordered_map但我也遇到了一些问题:

  • 我不能使用树节点本身作为键,所以我需要引入另一个类,它只引用树节点并在树
  • 中表示它们
  • 引用unordered_map的键值需要擦除其引用被删除的所有条目,或者我们在unordered_map中有一个悬空引用(/指针),这可能在rehash
  • 上被触发。
  • 树中的更改需要重建unordered_map,因为键可能已经更改

我错过了一些明显的解决方案吗?你喜欢哪种方法(为什么)?

我曾经遇到过类似的问题,我的解决方案如下:

  1. 让每个节点有一个唯一的标识符。
  2. 让每个节点都有一个版本号。使节点计算值无效的修改只会增加版本号。
  3. 让每个访问者有一个缓存映射,其中ID对是键,映射到版本/值对。
  4. 当(重新)遍历树时,在映射中查找节点的条目。如果版本正确,则使用缓存值。如果过期,计算新值并替换旧版本/值对。

起初,我使用节点的地址作为ID,但由于内存原因,我不得不重用子树并选择到节点的路径作为ID。这种路径的优点是它可以由每个访问者计算,而不需要存储在节点上。在我的例子中,每个节点最多可以有两个子节点,所以路径仅仅是一组左/右决定,它可以存储在一个简单的unsigned int中,并进行一些位移动(我的树从未达到32的深度,所以32位unsigned作为键就足够了)。