LRU缓存设计
最近最少使用(LRU)缓存是先丢弃最近最少使用的项 如何设计和实现这样一个缓存类?设计要求如下:
1)尽快找到该项目
2)一旦缓存未命中且缓存已满,我们需要尽快替换最近最少使用的项目。
如何从设计模式和算法设计角度来分析和实现这个问题?
Least Recently Used (LRU) Cache is to discard the least recently used items first
How do you design and implement such a cache class? The design requirements are as follows:
1) find the item as fast as we can
2) Once a cache misses and a cache is full, we need to replace the least recently used item as fast as possible.
How to analyze and implement this question in terms of design pattern and algorithm design?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(14)
链表 + 指向链表节点的指针的哈希表是实现 LRU 缓存的常用方法。这提供了 O(1) 操作(假设散列不错)。这样做的优点(O(1)):您可以通过锁定整个结构来实现多线程版本。您不必担心粒度锁定等问题。
简单地说,它的工作方式是:
在访问值时,将链表中的相应节点移动到头部。
当需要从缓存中删除一个值时,可以从尾部删除。
当你向缓存添加一个值时,只需将它放在链表的头部即可。
感谢 doublep,这里有一个 C++ 实现的网站:杂项容器模板。
A linked list + hashtable of pointers to the linked list nodes is the usual way to implement LRU caches. This gives O(1) operations (assuming a decent hash). Advantage of this (being O(1)): you can do a multithreaded version by just locking the whole structure. You don't have to worry about granular locking etc.
Briefly, the way it works:
On an access of a value, you move the corresponding node in the linked list to the head.
When you need to remove a value from the cache, you remove from the tail end.
When you add a value to cache, you just place it at the head of the linked list.
Thanks to doublep, here is site with a C++ implementation: Miscellaneous Container Templates.
这是我的 LRU 缓存的简单 C++ 实现示例,结合了哈希(unordered_map)和列表。列表上的项目有访问映射的键,地图上的项目有列表的迭代器来访问列表。
This is my simple sample c++ implementation for LRU cache, with the combination of hash(unordered_map), and list. Items on list have key to access map, and items on map have iterator of list to access list.
我在这里看到了一些不必要的复杂实现,因此我决定也提供我的实现。缓存只有两个方法,get和set。希望它具有更好的可读性和理解性:
I see here several unnecessary complicated implementations, so I decided to provide my implementation as well. The cache has only two methods, get and set. Hopefully it is better readable and understandable:
这是我的一个基本、简单的 LRU 缓存的实现。
Here is my implementation for a basic, simple LRU cache.
两年前我实现了一个线程安全的 LRU 缓存。
LRU 通常使用 HashMap 和 LinkedList 来实现。你可以谷歌一下实现细节。有很多关于它的资源(维基百科也有很好的解释)。
为了线程安全,每当修改 LRU 的状态时都需要加锁。
我将我的C++代码贴在这里供您参考。
这是实现。
这是单元测试。
I implemented a thread-safe LRU cache two years back.
LRU is typically implemented with a HashMap and LinkedList. You can google the implementation detail. There are a lot of resources about it(Wikipedia have a good explanation too).
In order to be thread-safe, you need put lock whenever you modify the state of the LRU.
I will paste my C++ code here for your reference.
Here is the implementation.
Here is the unit tests.
是否允许 LRU 近似?这是针对某些图像平滑算法每秒执行 2000 万次获取/设置操作的算法。我不知道它是否不是最糟糕的,但它肯定比 Javascript 更快,后者每秒仅执行 150 万次获取/设置。
Unordered_map 用于跟踪循环缓冲区上的项目。循环缓冲区不会像其他链表版本那样添加/删除节点。因此,它至少应该对 CPU 的 L1/L2/L3 缓存友好,除非缓存大小比这些缓存大得多。算法很简单。有一只时钟指针驱逐受害者槽位,而另一只手确实将其中一些槽位作为“第二次机会”从驱逐中拯救出来,但将驱逐阶段滞后了 50%,因此,如果缓存很大,则缓存项有很长的时间获得第二次机会/免遭驱逐。
由于这是一个近似值,因此您不应期望它总是驱逐最近的一个。但它确实可以加速某些比 RAM 慢的网络 I/O、磁盘读/写等。我在 VRAM 虚拟缓冲区类中使用了它,该类使用 100% 的系统视频 RAM(来自多个显卡)。 VRAM 比 RAM 慢,因此对于某些缓存友好的访问模式,在 RAM 中进行缓存使得 6GB VRAM 看起来与 RAM 一样快。
这是实现:
这是用法:
Is LRU approximation allowed? Here is one that does 20 million get/set operations per second for some image smoothing algorithm. I don't know if its not the worst but its certainly a lot faster than Javascript equivalent which does only 1.5 million get/set per second.
Unordered_map to keep track of items on circular buffers. Circular buffer doesn't add/remove nodes as other linked-list versions. So it should be at least friendly on the CPU's L1/L2/L3 caches unless cache size is much bigger than those caches. Algorithm is simple. There is a hand of clock that evicts victim slots while the other hand does save some of them from eviction as a "second chance" but lags the eviction by 50% phase so that if cache is big then cache items have a good amount of time to get their second chance / be saved from eviction.
Since this is an approximation, you shouldn't expect it to evict the least recent one always. But it does give a speedup on some network I/O, disk read/write, etc that are slower than RAM. I used this in a VRAM virtual buffer class that uses 100% of system video-ram (from multiple graphics cards). VRAM is slower than RAM so caching in RAM makes 6GB VRAM look like as fast as RAM for some cache-friendly access patterns.
Here is implementation:
Here is usage:
我在此处实现了 LRU。该接口遵循 std::map 所以它应该不难使用。此外,您还可以提供自定义备份处理程序,当数据在缓存中失效时使用该处理程序。
I have a LRU implementation here. The interface follows std::map so it should not be that hard to use. Additionally you can provide a custom backup handler, that is used if data is invalidated in the cache.
Java代码:
Java Code :
我们必须创建一个数据结构,使我们能够
同时优化所有三个主要操作。
现在我最喜欢的问题是,我们能做得更好吗?
参考
We have to create a data structure that allows us to
optimize all three main operations at the same time.
Now my favorite question is, can we do any better?
reference
缓存是不是像哈希表一样支持按键检索值的数据结构? LRU 意味着缓存有一定的大小限制,我们需要定期删除最少使用的条目。
如果你用链表+指针哈希表来实现,你怎么能通过键来进行 O(1) 检索值呢?
我将使用哈希表实现 LRU 缓存,其中每个条目的值是 value + 指向上一个/下一个条目的指针。
关于多线程访问,我更喜欢监视读写锁(最好通过自旋锁实现,因为争用通常很快)。
Is cache a data structure that supports retrieval value by key like hash table? LRU means the cache has certain size limitation that we need drop least used entries periodically.
If you implement with linked-list + hashtable of pointers how can you do O(1) retrieval of value by key?
I would implement LRU cache with a hash table that the value of each entry is value + pointers to prev/next entry.
Regarding the multi-threading access, I would prefer reader-writer lock (ideally implemented by spin lock since contention is usually fast) to monitor.
LRU页面替换技术:
当一个页面被引用时,所需的页面可能在缓存中。
如果在缓存中
:我们需要将其放到缓存队列的前面。如果不在缓存中
:我们将其放入缓存中。简单来说,我们将一个新页面添加到缓存队列的前面。如果缓存满了,即所有帧都满了,我们从缓存队列的后面删除一个页面,并将新页面添加到缓存队列的前面。输入/输出
LRU Page Replacement Technique:
When a page is referenced, the required page may be in the cache.
If in the cache
: we need to bring it to the front of the cache queue.If NOT in the cache
: we bring that in cache. In simple words, we add a new page to the front of the cache queue. If the cache is full, i.e. all the frames are full, we remove a page from the rear of cache queue, and add the new page to the front of cache queue.Input/Output
这是我的简单 Java 程序员,复杂度为 O(1)。
//
This is my simple Java programmer with complexity O(1).
//
详细说明请参见我的博文。
Detailed explanation here in my blogpost.
LRU 缓存的工作
首先丢弃最近最少使用的项目。如果想确保算法始终丢弃最近最少使用的项目,则该算法需要跟踪使用的内容,这是昂贵的。该技术的一般实现需要保留高速缓存行的“年龄位”,并根据年龄位跟踪“最近最少使用”的高速缓存行。在这样的实现中,每次使用高速缓存行时,所有其他高速缓存行的寿命都会改变。
以下示例的访问顺序为 ABCDECD B。
Working of LRU Cache
Discards the least recently used items first. This algorithm requires keeping track of what was used when which is expensive if one wants to make sure the algorithm always discards the least recently used item. General implementations of this technique require keeping "age bits" for cache-lines and track the "Least Recently Used" cache-line based on age-bits. In such an implementation, every time a cache-line is used, the age of all other cache-lines changes.
The access sequence for the below example is A B C D E C D B.