LRU 缓存,能够快速返回缓存中元素的位置

问题描述

对于数据压缩,我想用一个关于值最后出现时间的索引替换(长)列表中的值。所以列表:

18499   10123   5678   10123   10123   3344   10123   5678   4912   18499

将被替换如下:

N18449  N10123  N5678  K1      K0      N3344  K1      K2     N4912  K4

以前未见过的新值以 N 为前缀。已知的旧值以 K 为前缀。例如,第二次出现的 10123 被替换为 K1,因为还有一个其他值 ( 5678)之间。但是,为了保持索引尽可能低,我希望 K 索引不测量列表中的距离,而是测量值的最后一次出现和当前出现之间看到的唯一其他值的实际数量。因此,例如,第二次出现的 5678 被 K2 替换,因为它们之间还有两个其他值(10123 和 3344),即使 10123 重复了几次。同样,最后一个值 18499 被 K4 替换,因为在它和列表的开头(也是 18499)之间还有四个其他值。如果只测量距离,最后一个元素将是 K9。

乍一看,压缩/索引替换可以使用 LRU 缓存来完成,对此 stackoverflow 保存了一些非常好的引用(即在 LRU cache design 上)。不幸的是,经典的 LRU 缓存对于这个目的并不是很好:虽然查找,如果一个项目在最后 N 个(LRU 缓存大小)项目中,O(1) 的速度很快,实际 的查找一项在 LRU 缓存中的位置是 O(n)(n 是找到的元素之前的元素数)。

解压步骤同样缓慢,当需要再次用相应的值替换 Kn 时:遍历经典 LRU 缓存的链表以找到要替换的项目,需要 n 步。

我很清楚,我的问题不存在 O(1) 解决方案,因为每次将新元素添加到缓存或一个现有的被移到前面。但是有 O(log(n)) 解决方案吗?如果可能的话,我不想用 O(N)。

我想在 C++ 中实现它,但也将不胜感激任何其他编程语言中指向此类数据结构的指针。我在这里问的更多是关于算法而不是具体实现。

解决方法

目前我能想到的最佳解决方案是将已知值存储在双链表中(请记住,经典的 LRU 缓存由双链表组成,用于维护元素排序和哈希图)快速 O(1) 元素访问),但在树结构中。一个合适的树似乎是在 github 上找到的 Countertree 2.0 库:https://github.com/fjtapia/countertree_2.0

Countertree 2.0 库提供了树数据结构的经典 mapmultimapsetmultiset 变体。但由于该库还在每个树节点维护计数器,因此它允许快速 O(log(n)) 索引访问和两个位置/迭代器之间的距离计算。

因此,我想用 list 替换标准 LRU 缓存的双链接 countertree::multiset。当所有元素比较相等时,multiset 实际上会退化。新元素总是插入在 multiset 的前面。由于树中元素的相对顺序永远不会改变,因此所有元素相对于它们的插入时间保持它们的相对位置。如果在构建树的过程中重新访问值,它们将从当前树位置取消链接并重新插入到前面。所以每个值在多重集中只会出现一次。

为了快速找到已使用的值,multiset 中的位置(= 迭代器)根据它们在单独哈希图中的值进行索引(非常像经典的 LRU 缓存由链表和哈希图组成) .如果稍后通过该哈希图再次找到值,则通过所述 O(log(n)) 距离计算测量其到 multiset 前面的距离,然后将其从 {{ 1}} 并在前面重新插入(这是另一个 O(log(n)) 操作,这基本上是访问与距离计算相同的树节点,因此它不应该需要更多的内存访问)。>

在解压期间,通过索引在多集中查找值,这再次发生在 O(log(n)) 时间内。

所以一切都很完美。还是我忽略了什么?或者有更好的解决方案吗?

,

另一种方法是保留一个位图,其中列表位置是缓存中某个元素最近出现的位置,加上一个段树,用于在一个范围内进行有效的popcounts。 (Fenwick 树也是一种选择,但是你总是为查询支付 Ω(height) 并且需要一个奇怪的非均匀存储方案来最小化空间。)

得到的结构有

  • unordered_map 从元素到(最近出现的索引,LRU 列表迭代器)
  • list 从最近使用到最近使用
  • 如上所述分割树。

要处理一个元素,请在 unordered_map 中查找它。如果存在,清除段树中前一次出现的位,创建一个列表条目,并在需要时弹出列表的后面(清除段树中的索引)。将当前元素移动到 LRU 列表的前面,将其索引复制到 unordered_map 并在段树中设置该索引。

unordered_maplist 操作预期摊销 O(1),因此段树驱动渐近成本。这个代价理论上是 Θ(log n),但在实践中,locality 比树好得多。我们可以用 popcount 替换段树的底层,并使用 k-ary 段树代替 2-ary 来减少随机内存访问。

缺点是管理段树的空间使用。也许你能负担得起整个输入的位图,在这种情况下,太好了!否则,您可能需要对 LRU 列表和关联的数据结构进行线性传递,以在保持顺序的同时对索引进行连续重新编号。这不会影响摊销成本,因为它只需要在您处理了足够多的元素来证明其合理性时发生。