リリース・改善中
Algorithm
LRUキャッシュは満杯になると最も長く使われていない項目を捨てるキャッシュで、ハッシュマップと双方向連結リストで get と put を O(1) にします。
LRU(Least Recently Used)キャッシュは、容量が決まったキャッシュが満杯になったとき、最も長い間使われていない項目から追い出す置換方式とそのデータ構造です。最近使ったデータはすぐまた使われやすいという時間的局所性を前提とし、FIFOと違って読み出すたびに項目の順位を更新します。標準的な実装は、キーからノードを引くハッシュマップと、使用順を保つ双方向連結リストを組み合わせ、get と put をどちらも平均 O(1) で処理します。
LRUはそのままの形や近似の形で、CPUキャッシュ、OSのページキャッシュ、データベースのバッファプール、Redisのメモリ追い出しポリシー、CDNやブラウザのキャッシュなど、システムのほぼすべての層で使われています。Pythonの functools.lru_cache や OrderedDict、Javaの LinkedHashMap のように標準ライブラリにも含まれています。ハッシュマップと連結リストを一つの設計で組み合わせるため、コーディング面接の定番問題でもあります。
まずFIFO、LRU、LFU、TTLといった置換方式の違いとヒット率の考え方を押さえ、短いリクエスト列を手で追ってみるとよいでしょう。次に、番兵ノードを置いた双方向連結リストと辞書で自分で実装し、OrderedDict や LinkedHashMap などのライブラリ版と比べます。最後に、スレッド安全性、キャッシュの無効化、有効期限との組み合わせといった実務的な問題に取り組みます。
get や put が成功するたびに項目を最新側へ移し、空きが足りなくなると反対側の端の項目を追い出します。
マップはキーからノードを直接見つけ、リストはノードを O(1) で外して先頭に付け直し、順序を保ちます。
get も put も決まった数のポインタだけを書き換えるので平均定数時間で終わり、空間は容量に比例します。
実際のシステムでは、コストを減らしたり大きなスキャンに強くしたりするため、CLOCK、擬似LRU、サンプリングLRU、2Q、W-TinyLFU などがよく使われます。
dict がキーをノードに対応づけ、番兵 head と tail の間の双方向連結リストが使用順を保ちます。get はノードを先頭へ移し、put は満杯のとき tail の直前のノードを追い出します。python lru_cache.py を実行すると "b" が追い出され、None 1 3 と表示されます。
lru_cache.py
class Node:
__slots__ = ("key", "value", "prev", "next")
def __init__(self, key=None, value=None):
self.key, self.value = key, value
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.map = {} # key -> Node
self.head, self.tail = Node(), Node() # sentinels
self.head.next, self.tail.prev = self.tail, self.head
def _unlink(self, node):
node.prev.next, node.next.prev = node.next, node.prev
def _push_front(self, node):
node.prev, node.next = self.head, self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key, default=None):
node = self.map.get(key)
if node is None:
return default
self._unlink(node) # touch: move to the front
self._push_front(node)
return node.value
def put(self, key, value):
if self.capacity <= 0:
return
node = self.map.get(key)
if node is not None:
node.value = value
self._unlink(node)
self._push_front(node)
return
if len(self.map) >= self.capacity: # evict the least recently used
lru = self.tail.prev
self._unlink(lru)
del self.map[lru.key]
node = Node(key, value)
self.map[key] = node
self._push_front(node)
cache = LRUCache(2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a") # "a" is now the most recent
cache.put("c", 3) # full: evicts "b"
print(cache.get("b"), cache.get("a"), cache.get("c")) # None 1 3
python lru_cache.pyインストールから LRUキャッシュ の中心となる考え方まで、6 章で順を追って学びます。
LRUキャッシュ について質問し、経験を共有し、意見を交わす場所です。
まだディスカッションはありません。最初の話題を始めましょう。
コメント 0件
ログイン · ログインするとコメントできます。
最初のコメントを書いてみましょう。