【问题标题】:Least Recently Used (LRU) Cache最近最少使用 (LRU) 缓存
【发布时间】:2017-08-09 07:33:53
【问题描述】:

我知道我可以在 STL 中使用各种容器类,但这样做的代价太大而且成本很高。

我们有超过 100 万在线用户,每个用户我们需要维护 8 个不相关的 32 位数据项。目标是

  1. 查找列表中是否存在项目,
  2. 如果没有,请插入。如果已满,则删除最旧的条目。

蛮力方法是维护最后一个写入指针并进行迭代(因为只有 8 个项目),但我正在寻找输入以更好地分析和实施。

期待在设计模式和算法方面提出一些有趣的建议。

【问题讨论】:

  • CC++?答案可能会有很大差异......
  • 别打扰了。 1M 用户 * 8 个字段/用户 * 4 字节/字段 = 32 MBytes。在一个体面的服务器处理器上,相关数据将一直在 L3 缓存中。任何算法都是矫枉过正。编写干净、简单、可维护的代码并继续前进。最好花时间(您会浪费在过早的优化上)为您的用户实现另一个功能。
  • 我推荐一个简单的数组,对项目进行线性搜索(并将项目与 32 字节边界对齐,因此它永远不会越过缓存线)。按到货顺序存放物品,如果已满,则先取出并转移。我认为任何复杂的算法都不会明显更快。
  • 您可能需要澄清一些细节。我假设您的意思是“给定用户的 8 个数据项中最旧的数据项”。而且我不知道您是否想要插入的年龄,或者最后一次查找的年龄。 LRU 建议后者,但您建议的实现是前者。
  • @drop 谢谢,我同意。我们当前的实现使用线性数组,但正在探索是否有更好的可能性。

标签: c++ c algorithm data-structures


【解决方案1】:

Don Knuth 在计算机编程的艺术中给出了几个有趣且非常有效的近似值。

  1. 自组织列表I:找到条目后,将其移至列表头部;从最后删除。
  2. 自组织列表二:找到条目后,将其上移一位;从最后删除。

    [以上两者都在卷。 3 §6.1(A)。]

  3. 另一种方案涉及循环维护列表,每个条目有 1 个额外位,当您找到该条目时设置该位,并在您跳过它以查找其他内容时清除。你总是从你停止的最后一个地方开始搜索,如果你没有找到条目,你用它替换下一个清晰的位,即自从整个列表之旅以来它没有被使用过。

    [卷。 1 §2.5(G)。]

【讨论】:

    【解决方案2】:

    您想在这里使用Hash 表和doubly linked list 的组合。
    每个项目都可以通过哈希表访问,该哈希表包含您需要的键以及指向列表中元素的指针。

    算法:

    给定新项目x,执行:
    1.将x添加到列表的头部,指针保存为ptr
    2、将x添加到存储数据的哈希表中,并添加ptr
    3. 如果列表比允许的大,取出最后一个元素(从列表的尾部)并删除它。也可以使用该元素的键将其从哈希表中删除。

    【讨论】:

    • 不确定这是否会比蛮力线性搜索和移位更快,因为它只有 8 个 32 位项。
    • 谢谢,但是正如我所提到的,任何列表或地图对于 8 个条目来说都是多余的,并且不会被优化。
    【解决方案3】:

    如果你想要 LRU 缓存的 C 实现,试试这个link

    这个想法是我们使用两种数据结构来实现一个 LRU Cache。

    使用双向链表实现的队列。队列的最大大小将等于可用帧的总数(缓存大小)。最近使用的页面将靠近前端,最近最少使用的页面将靠近后端。
    以页码为键,以相应队列节点的地址为值的 Hash。
    当一个页面被引用时,所需的页面可能在内存中。如果它在内存中,我们需要将链表的节点分离出来,放到队列的最前面。
    如果所需的页面不在内存中,我们将其带入内存。简单来说,就是在队列的最前面增加一个新节点,并更新哈希中对应的节点地址。如果队列已满,即所有帧都已满,我们从队列尾部移除一个节点,并将新节点添加到队列前端。

    【讨论】:

      【解决方案4】:

      我个人会选择 EJP 提出的自组织列表,或者由于我们只有八个元素,因此只需将它们与时间戳一起按顺序存储。

      访问一个元素时,只需更新时间戳,替换时,用最旧的时间戳替换一个(一次线性搜索)。这在替换时效率较低,但在访问时效率更高(无需移动任何元素)。而且它可能是最容易实现的......

      修改自组织列表,如果基于某些数组数据结构:当然,在更新时,您必须移动几个元素(变体 I)或至少交换其中两个(变体 II) - 但如果您组织数据作为环形缓冲区,在替换时,我们只需将最后一个元素替换为新元素并将缓冲区的指针移动到这个新元素:

      a, b, c, d
            ^
      

      访问一个:

      d, b, a, c
            ^
      

      新元素 e:

      d, e, a, c
         ^
      

      特殊情况:访问最旧的元素(在本例中为 d) - 然后我们也可以简单地移动指针:

      d, e, a, c
      ^
      

      只是:只有 8 个元素,可能不值得努力实现所有这些......

      【讨论】:

        【解决方案5】:

        我同意 Drop 和 Geza 的 cmets。直接的实现将读取一个缓存行,并导致一个缓存行写入。

        剩下的唯一性能问题是在 256 位中查找和更新该 32 位值。假设现代 x86,查找本身可以是两条指令:_mm256_cmp_epi32_mask 并行查找所有相等的值,_mm256_lzcnt_epi32 计算前导零 = 旧的不匹配项的数量*32。但即使使用较旧的 SIMD 操作,缓存行读/写操作仍将主导执行时间。而这反过来又取决于找到合适的用户。而这又由所涉及的网络 I/O 主导。

        【讨论】:

          【解决方案6】:

          您应该使用Cuckoo's Filter,它是一种概率数据结构,支持快速set 成员资格测试。它是一个基于hash 的数据结构。

          杜鹃滤波器的时间复杂度:

          1. 查找:O(1)
          2. 删除:O(1)
          3. 插入:O(1)

          这里是布谷鸟过滤器的工作原理供参考。

          Parameters of the filter
          
           1. Two Hash Functions: h1 and h2
           2. An array B with n Buckets. The i-th Bucket will be called B[i]
          
          Input : L, a list of elements to inserted into the cuckoo filter.
          
          Algorithm:
          while L is not empty:
              Let x be the 1st item in the list L. Remove x from the list.
              if (B[h1(x)] == empty)
                    place x in B[h1(x)];
              else if (B[h2(x)] == empty)
                    place x in B[h2(x)];
              else
                    Let y be the element in B[h2(x)]
                    Prepend y to L
                    place x in B[h2(x)]
          

          对于 LRU,您可以通过仅保留一个局部变量在哈希函数中使用时间戳。

          这是迄今为止处理超大型数据集的最佳方法。

          【讨论】:

          • 鉴于每个用户只有 O(8) 个条目,所有操作都是 O(1)。这不是一个非常大的数据集,一点也不。
          • @MSalters 每个用户有 8 个条目,问题说有超过 1M 的用户,所以有 8 x 1M 的条目。所以这就是为什么我认为它有一个庞大的数据集
          • 那你的答案就不清楚了。您是否建议在单个 Cuckoo Filter 中存储 800 万个条目?这当然是可能的,您只需向用户 XYZ 提供条目 XYZ-0 到 XYZ-7。您可以给它们中的每一个加上时间戳,以确定 8 个条目中的哪一个是最旧的,并且需要被替换。然而,这远非有效。内存开销巨大,并且完全没有引用局部性。
          • 谢谢老爷。有 100 万个用户,他们已经在 hash map 中,所以一旦我们得到用户条目,我们只需要管理这 8 个条目。
          猜你喜欢
          • 1970-01-01
          • 2011-04-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-09-17
          相关资源
          最近更新 更多