我应该为该缓存策略使用哪种数据结构?


11

我正在使用.NET 4.0应用程序,该应用程序在返回双精度数的两个双精度数上执行相当昂贵的计算。对数千项中的每一项执行此计算。这些计算是在Task线程池线程上执行的。

一些初步测试表明,一次又一次地执行相同的计算,因此我想缓存n个结果。当缓存满了,我想抛出了最不经常最近使用的项目。(编辑:我意识到至少经常没有意义,因为当缓存已满并且我将结果替换为新计算的结果时,那个结果将是最不常用的,并在下次计算新结果时立即替换并添加到缓存中)

为了实现这一点,我正在考虑使用Dictionary<Input, double>(其中Input将存储两个输入double值的微型类)存储输入和缓存的结果。但是,我还需要跟踪上次使用结果的时间。为此,我认为我需要第二个存储信息的集合,以便在缓存已满时从字典中删除结果。我担心不断对清单进行排序会对性能产生负面影响。

有没有更好的方法(例如性能更高的方法),或者甚至是我不知道的通用数据结构?为了确定解决方案的最优性,我应该进行哪些类型的分析/测量?

Answers:


12

如果您想使用LRU移出缓存(最近最少使用的移出),则可能要使用的数据结构的好组合是:

  • 循环链表(作为优先级队列)
  • 字典

这就是为什么:

  • 链表的插入和删除时间为O(1)
  • 当列表已满且无需执行任何额外分配时,可以重新使用列表节点。

这是基本算法的工作方式:

数据结构

LinkedList<Node<KeyValuePair<Input,Double>>> list; Dictionary<Input,Node<KeyValuePair<Input,Double>>> dict;

  1. 收到输入
  2. 如果字典包含密钥
    • 返回存储在节点中的值,并将节点移至列表的开头
  3. 如果字典不包含密钥
    • 计算价值
    • 将值存储在列表的最后一个节点中
    • 如果最后一个没有值,请从字典中删除前一个键
    • 将最后一个节点移到第一个位置。
    • 将(输入,节点)键值对存储在字典中。

这种方法的一些好处是,读取和设置字典值接近O(1),在链表中插入和删除节点为O(1),这意味着算法正在接近O(1)以读取和写入值到高速缓存,并避免了内存分配和阻止内存复制操作,从内存角度来看使其稳定。


好点,到目前为止最好的主意,恕我直言。我今天基于此实现了一个缓存,因此必须进行概要分析,看看明天的性能如何。
PersonalNexus

3

鉴于您在普通PC上拥有的处理能力,这似乎需要花费很多精力进行一次计算。同样,您仍然要为每个唯一的值对花费第一次调用计算的费用,因此,100,000个唯一值对仍将花费您最少n * 100,000的时间。考虑一下,随着字典的增大,访问字典中的值的速度可能会变慢。您能否保证您的词典访问速度能够补偿到足以为您的计算速度提供合理的回报?

无论如何,听起来您似乎可能需要考虑寻找一种优化算法的方法。为此,您将需要一个分析工具,例如Redgate Ants,以查看瓶颈在哪里,并帮助您确定是否有方法可以减少与类实例化,列表遍历和数据库有关的某些开销。访问,或者花费太多时间。


1
不幸的是,暂时无法更改计算算法,因为它是使用一些高级数学的第三方库,这自然会占用大量CPU。如果以后会进行修改,我一定会检查建议的配置工具。此外,计算将经常执行,有时使用相同的输入,因此即使使用非常幼稚的缓存策略,初步分析也显示出明显的好处。
PersonalNexus

0

一种想法是为什么只缓存n个结果?即使n为300,000,您也只会使用7.2MB的内存(加上表结构的任何额外开销)。当然,假设三个64位双打。如果您不担心内存空间不足,则可以简单地将备忘录应用于复杂的计算例程本身。


我要分析的不仅仅是缓存,而是每个“项目”一个,可能有数十万个这样的项目。
PersonalNexus

输入来自哪个“项目”有什么关系?有副作用吗?
jk。

@jk。不同的项目将对计算产生非常不同的输入。由于这意味着几乎没有重叠,因此我认为将它们保留在单个缓存中没有意义。此外,不同的项可能位于不同的线程中,因此为了避免共享状态,我想将缓存分开。
PersonalNexus

@PersonalNexus我认为这意味着计算中涉及了2个以上的参数?在其他方面,您基本上仍然有f(x,y)=做一些事情。再加上共享状态似乎会帮助性能而不是阻碍?
彼得·史密斯

@PeterSmith这两个参数是主要输入。还有其他人,但很少改变。如果他们这样做,我将丢弃整个缓存。“共享状态”是指所有或一组项目的共享缓存。由于这将需要以其他方式锁定或同步,因此会影响性能。更多关于共享状态对性能的影响
PersonalNexus

0

第二个集合的方法很好。它应该是一个优先级队列,它可以快速查找/删除最小值,还可以更改(增加)队列中的优先级(后一部分是困难的,大多数简单的prio队列实现不支持)。在C5库有这样的集合,它被称为IntervalHeap

或者,当然,您可以尝试构建自己的收藏集,例如 SortedDictionary<int, List<InputCount>>。(InputCount必须是将您的Input数据与您的Count价值相结合的类)

更改数值时更新该集合可以通过删除并重新插入元素来实现。


0

正如彼得史密斯(Peter Smith)的答案所指出的那样,您尝试实现的模式称为备忘录。在C#中,以透明的方式实现无副作用的备忘录非常困难。奥利弗·斯特姆(Oliver Sturm)的C#函数编程书籍提供了一种解决方案(可下载代码,第10章)。

在F#中会容易得多。当然,开始使用其他编程语言是一个重大决定,但可能值得考虑。特别是在复杂的计算中,它必然会使更多的事情比记忆更容易编程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.