設計並實現一個LRU Cache
一、什麼是Cache
1 概念
Cache,即快取記憶體,是介於CPU和記憶體之間的高速小容量儲存器。在金字塔式儲存體系中它位於自頂向下的第二層,僅次於CPU暫存器。其容量遠小於記憶體,但速度卻可以接近CPU的頻率。
當CPU發出記憶體訪問請求時,會先檢視 Cache 內是否有請求資料。
- 如果存在(命中),則直接返回該資料;
- 如果不存在(失效),再去訪問記憶體 —— 先把記憶體中的相應資料載入快取,再將其返回處理器。
提供“快取記憶體”的目的是讓資料訪問的速度適應CPU的處理速度,通過減少訪問記憶體的次數來提高資料存取的速度。
2 原理
Cache 技術所依賴的原理是”程式執行與資料訪問的區域性性原理
- 時間區域性性:如果程式中的某條指令一旦執行,不久以後該指令可能再次執行,如果某資料被訪問過,不久以後該資料可能再次被訪問。
- 空間區域性性:一旦程式訪問了某個儲存單元,在不久之後,其附近的儲存單元也將被訪問,即程式在一段時間內所訪問的地址,可能集中在一定的範圍之內,這是因為指令或資料通常是順序存放的。
時間區域性性是通過將近來使用的指令和資料儲存到Cache中實現。空間區域性性通常是使用較大的快取記憶體,並將 預取機制 整合到快取記憶體控制邏輯中來實現。
3 替換策略
Cache的容量是有限的,當Cache的空間都被佔滿後,如果再次發生快取失效,就必須選擇一個快取塊來替換掉。常用的替換策略有以下幾種:
隨機演算法(Rand):隨機法是隨機地確定替換的儲存塊。設定一個隨機數產生器,依據所產生的隨機數,確定替換塊。這種方法簡單、易於實現,但命中率比較低。
先進先出演算法(FIFO, First In First Out):先進先出法是選擇那個最先調入的那個塊進行替換。當最先調入並被多次命中的塊,很可能被優先替換,因而不符合區域性性規律。這種方法的命中率比隨機法好些,但還不滿足要求。
最久未使用演算法(LRU, Least Recently Used):LRU法是依據各塊使用的情況, 總是選擇那個最長時間未被使用的塊替換。這種方法比較好地反映了程式區域性性規律。
最不經常使用演算法(LFU, Least Frequently Used)
4 概念的擴充
如今快取記憶體的概念已被擴充,不僅在CPU和主記憶體之間有Cache,而且在記憶體和硬碟之間也有Cache(磁碟快取),乃至在硬碟與網路之間也有某種意義上的Cache──稱為Internet臨時資料夾或網路內容快取等。凡是位於速度相差較大的兩種硬體之間,用於協調兩者資料傳輸速度差異的結構,均可稱之為Cache。
二、LRU Cache的實現
Google的一道面試題:
Design an LRU cache with all the operations to be done in
1 思路分析
對一個Cache的操作無非三種:插入(insert)、替換(replace)、查詢(lookup)。
為了能夠快速刪除最久沒有訪問的資料項和插入最新的資料項,我們使用 雙向連結串列 連線Cache中的資料項,並且保證連結串列維持資料項從最近訪問到最舊訪問的順序。
插入:當Cache未滿時,新的資料項只需插到雙鏈表頭部即可。時間複雜度為
O(1) .替換:當Cache已滿時,將新的資料項插到雙鏈表頭部,並刪除雙鏈表的尾結點即可。時間複雜度為
O(1) .查詢:每次資料項被查詢到時,都將此資料項移動到連結串列頭部。
經過分析,我們知道使用雙向連結串列可以保證插入和替換的時間複雜度是
2 程式碼實現
從上述分析可知,我們需要使用兩種資料結構:
- 雙向連結串列(Doubly Linked List)
- 雜湊表(Hash Table)
下面是LRU Cache的 C++ 實現:
#include <iostream>
#include <unordered_map>
using namespace std;
// 雙向連結串列的節點結構
struct LRUCacheNode {
int key;
int value;
LRUCacheNode* prev;
LRUCacheNode* next;
LRUCacheNode():key(0),value(0),prev(NULL),next(NULL){}
};
class LRUCache
{
private:
unordered_map<int, LRUCacheNode*> m; // 代替hash_map
LRUCacheNode* head; // 指向雙鏈表的頭結點
LRUCacheNode* tail; // 指向雙鏈表的尾結點
int capacity; // Cache的容量
int count; // 計數
public:
LRUCache(int capacity); // 建構函式
~LRUCache(); // 解構函式
int get(int key); // 查詢資料項
void set(int key, int value); // 未滿時插入,已滿時替換
private:
void removeLRUNode(); // 刪除尾結點(最久未使用)
void detachNode(LRUCacheNode* node); // 分離當前結點
void insertToFront(LRUCacheNode* node); // 節點插入到頭部
};
LRUCache::LRUCache(int capacity)
{
this->capacity = capacity;
this->count = 0;
head = new LRUCacheNode;
tail = new LRUCacheNode;
head->prev = NULL;
head->next = tail;
tail->prev = head;
tail->next = NULL;
}
LRUCache::~LRUCache()
{
delete head;
delete tail;
}
int LRUCache::get(int key)
{
if(m.find(key) == m.end()) // 沒找到
return -1;
else
{
LRUCacheNode* node = m[key];
detachNode(node); // 命中,移至頭部
insertToFront(node);
return node->value;
}
}
void LRUCache::set(int key, int value)
{
if(m.find(key) == m.end()) // 沒找到
{
LRUCacheNode* node = new LRUCacheNode;
if(count == capacity) // Cache已滿
removeLRUNode();
node->key = key;
node->value = value;
m[key] = node; // 插入雜湊表
insertToFront(node); // 插入連結串列頭部
++count;
}
else
{
LRUCacheNode* node = m[key];
detachNode(node);
node->value = value;
insertToFront(node);
}
}
void LRUCache::removeLRUNode()
{
LRUCacheNode* node = tail->prev;
detachNode(node);
m.erase(node->key);
--count;
}
void LRUCache::detachNode(LRUCacheNode* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
}
void LRUCache::insertToFront(LRUCacheNode* node)
{
node->next = head->next;
node->prev = head;
head->next = node;
node->next->prev = node;
}