小而简单的缓存的最佳数据结构是什么

时间:2009-07-08 06:26:16

标签: data-structures coding-style

我经常需要相对较小(<10000条目&lt; 1kb)的缓存来加速计算。我通常的代码如下所示:

cache = {}
def calculate_caches(parms):
    if parms not in cache:
        cache[parms] = calculate(parms)
    return cache[parms]

工作正常,但对于运行时间较长的进程,我担心内存泄漏。所以我经常像这样实施强力记忆夹:

if len(cache) > 1000:
    cache = {}

在大多数情况下工作得相当好,仍然是干净,简单的代码。但如果我想要一个真正的LRU strategy我需要时间戳和缓存条目。使用dict的问题是,使缓存到期现在意味着遍历整个缓存,既不优雅也不高效。

cache = {}
def calculate_caches(parms):
    if parms not in cache:
        cache[parms] = (time.time(), calculate(parms))
    expire()
    return cache[parms][1]

def expire()
    if len(cache) > 1000:
        mintime = time.time()
        time2key = {}
        for key, (timestamp, val) in cache.items():
            mintime = min([mintime, timestamp])
            time2key[timestamp] = key
        if mintime in time2key:
            del cache[time2key[mintime]]

是否有更好的方法/数据结构来实现ad-hoc缓存?

我的问题与this question非常相似,但我不需要按时间对列表进行排序,而且我不想要欺骗。

3 个答案:

答案 0 :(得分:1)

在不使用时间戳的情况下执行此操作的一种非常简单的方法是使用ordered dictionary,在最后有MRU(这是对同一个对象的请求来的时候)第二次,你删除它并在dict结束时再次添加)所以,当你需要过期时,如果大小超过限制,你只需从有序字典的开头删除一个X大小的片段。

效率现在取决于有序dict的实现方式。

答案 1 :(得分:1)

您可以查看Java的LinkedHashMap和LinkedHashSet以获取灵感。基本上它维护了一个双重链接列表,用于插入和可选的访问顺序。

要维护LRU,您可以定义策略,以便在插入新条目时删除最旧的条目(靠近链表的头部)。

答案 2 :(得分:0)

我怀疑这是一个金色的子弹;最优策略很大程度上取决于缓存未命中的成本以及计算参数的时间分布。

垃圾收集方法可能会给你一些启发。如果您将缓存视为堆,并将缓存命中为引用,那么您就会遇到使用(非零)命中数来有效收集缓存结果的问题。这个问题比GC更宽容,因为你可以重新计算你所做的任何事情。

对此方法的一种改进就是为频繁命中的参数引入额外的缓存。为缓存命中时递增的每个缓存值添加一个计数器。传递某个阈值时,缓存的值将提升为其他缓存。两代缓存都可以进行大小限制,因此您仍然对内存使用有严格的限制。这是一个经验问题,如果缓存未命中的(可能的)减少证明了开销(在两个缓存中查找,命中计数器,复制等)......