最後一篇想不到要寫什麼跟vSAN相關的主題,不如就來寫一個和Cache相關的主題吧!畢竟他和vSAN一樣都是計算機儲存的範疇
在我們的電腦系統中,Cache(快取)扮演著至關重要的角色。快取是一種高效能的臨時儲存區域,用於儲存經常被訪問的資料,以加速資料讀取和寫入的速度。想像一下冰箱裡的食物,快取就像是你放在冰箱門邊的零食,方便隨時取用,而不是藏在冰箱深處的冷凍食品。
當然,快取的速度很快,相對應的缺點就是「貴」!
在快取策略中,LRU(Least Recently Used)是一種經典的演算法。LRU 的基本策略是當快取空間不足時,淘汰最久未被使用的資料。這就像在冰箱裡,定期清理那些已經很久沒吃過的食物,確保新的食物有地方放。
要實現 LRU,我們需要兩個主要的資料結構:
# 定義 Doubly Linked List
class Node:
def __init__(self, key, value):
self.key = key # 一個整數key,佔用 4 bytes
self.value = value # 通常是一個pointer,佔用 8 bytes
self.prev = None # 前驅指標,佔用 8 bytes
self.next = None # 後繼指標,佔用 8 bytes
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
# key: int value
# value: Node address
self.cache = {}
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key: int) -> int:
if key in self.cache:
node = self.cache[key]
self._remove(node)
self._add(node)
return node.value
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
self._remove(self.cache[key])
node = Node(key, value)
self._add(node)
self.cache[key] = node
if len(self.cache) > self.capacity:
node_to_remove = self.head.next
self._remove(node_to_remove)
del self.cache[node_to_remove.key]
def _remove(self, node):
prev = node.prev
next = node.next
prev.next = next
next.prev = prev
def _add(self, node):
prev = self.tail.prev
prev.next = node
node.prev = prev
node.next = self.tail
self.tail.prev = node
LRU 的記憶體用量主要來自兩部分:
每個節點在雙向鏈表中需要儲存前驅(prev)和後繼(next)指標,這增加了記憶體消耗。隨著資料規模增大,這些額外的儲存開銷在系統變大所需快取增加後會變得非常顯著。
當快取規模增大時,雙向鏈表和哈希表的節點數量也會隨之增加。這意味著需要更多的記憶體來儲存這些額外的指標和哈希表條目。特別是在高併發環境下,管理這些資料結構會導致更多的記憶體碎片和開銷。隨著摩爾定律,現在的系統越來越大、資源越來越多,可用的Cache空間也是越來越大,那有沒有什麼演算法能同時達到LRU的cache命中率,並且隨著Cache增大還能減少記憶體的佔用呢?
S3-FIFO(simple, scalable, space-efficient first in first out)是一種新穎的快取管理策略,旨在解決 LRU 記憶體用量大的缺點。S3-FIFO 使用三個隊列(Queue):S快取、M快取、和G快取來管理資料。這些隊列的具體運作如下:
1. S快取:這個隊列用來儲存新插入的資料。當資料被訪問時,訪問計數會增加。如果S快取滿了,就會根據訪問計數決定是將資料移動到M快取還是G快取。
2. M快取:這個隊列用來儲存經常被訪問的資料。當M快取滿了時,最舊的資料會被移出。這些資料會根據訪問計數決定是否重新插入M快取或移至G快取。
3. G快取:這個隊列用來儲存那些在S快取和M快取中被淘汰的資料鍵值(不儲存實際資料和訪問計數)。它用來記錄哪些資料最近被淘汰,以防止頻繁的重複插入(重複的記憶體分配和釋放)。
假設我們有一個 S3-FIFO 快取系統,S 容量為 3,M 容量為 5,G 容量為 2。以下是一個運作過程的例子:
這個例子展示了 S3-FIFO 如何通過使用三個隊列來管理資料的插入和淘汰。S 隊列用於臨時儲存新插入的資料,M 隊列用於儲存經常訪問的資料,G 隊列用於記錄被淘汰的資料鍵值。這樣的設計不但確保了高效的快取管理和訪問,還減少了快取未命中的情況。
class CacheItem:
def __init__(self, key, value):
self.key = key
self.value = value
self.access_count = 0
class S3FIFO:
def __init__(self, capacity_s, capacity_m, capacity_g):
self.capacity_s = capacity_s
self.capacity_m = capacity_m
self.capacity_g = capacity_g
self.cache_s = []
self.cache_m = []
self.cache_g = {}
def get(self, key):
# 檢查 M 隊列
for item in self.cache_m:
if item.key == key:
item.access_count += 1
return item.value
# 檢查 S 隊列
for item in self.cache_s:
if item.key == key:
item.access_count += 1
return item.value
# 檢查 G 隊列
if key in self.cache_g:
item = self.cache_g.pop(key)
self.cache_s.append(item) # 重新插入 S 隊列
if len(self.cache_s) > self.capacity_s:
self.evict_s()
return item.value
return -1 # 如果 key 不在快取中,返回 -1
def put(self, key, value):
# 檢查 key 是否在 M 隊列中
for item in self.cache_m:
if item.key == key:
item.value = value
return
# 檢查 key 是否在 S 隊列中並更新
for item in self.cache_s:
if item.key == key:
item.value = value
return
# 檢查 key 是否在 G 隊列中
if key in self.cache_g:
item = self.cache_g.pop(key)
item.value = value
self.cache_s.append(item) # 重新插入 S 隊列
if len(self.cache_s) > self.capacity_s:
self.evict_s()
return
# 如果 key 不在任何快取中,則插入到 S 隊列
new_item = CacheItem(key, value)
self.cache_s.append(new_item)
if len(self.cache_s) > self.capacity_s:
self.evict_s()
def evict_s(self):
item = self.cache_s.pop(0)
if item.access_count > 0:
self.cache_m.append(item)
if len(self.cache_m) > self.capacity_m:
self.evict_m()
else:
self.cache_g[item.key] = item
if len(self.cache_g) > self.capacity_g:
self.evict_g()
def evict_m(self):
while len(self.cache_m) > self.capacity_m:
item = self.cache_m.pop(0)
if item.access_count > 0:
item.access_count -= 1
self.cache_m.append(item)
else:
self.cache_g[item.key] = item
if len(self.cache_g) > self.capacity_g:
self.evict_g()
def evict_g(self):
if self.cache_g:
oldest_key = next(iter(self.cache_g))
del self.cache_g[oldest_key]
if __name__ == '__main__':
cache = S3FIFO(capacity_s=3, capacity_m=5, capacity_g=2)
cache.put(1, 'A')
cache.put(2, 'B')
cache.put(3, 'C')
print(cache.get(1)) # 輸出: 'A'(增加訪問次數)
cache.put(4, 'D')
cache.put(5, 'E')
cache.put(6, 'F')
print(cache.get(2)) # 輸出: 'B'(資料 2 仍在 M 隊列中)
print(cache.get(3)) # 輸出: None(資料 3 被移到 G 隊列後再次插入 S 隊列)
# 更新資料
cache.put(1, 'AA')
print(cache.get(1)) # 輸出: 'AA'(更新後的值)
S3-FIFO 相比 LRU,雖然有其優勢,但也存在一些缺點和潛在的限制:
舉例說明
假設我們有一個快取系統,需要處理一系列訪問模式變化較大的資料:
S3-FIFO 以其簡單、高效和低資源消耗的特點,為快取管理提供了一個優秀的選擇。相比LRU,S3-FIFO更適合在大Cache的環境,但是針對訪問模式變化較大的資料或是系統,還是可以考慮原有的LRU快取策略。
如果喜歡我的文章的話,歡迎追蹤我的部落格: https://kaichiachen.github.io/2024/04/02/s3-fifo/