什么是LRU Cache
LRU是Least Recently Used的縮寫,意思是最近最少使用,它是一種Cache替換算法。 什么是Cache?狹義的Cache指的是位於CPU和主存間的快速RAM, 通常它不像系統主存那樣使用DRAM技術,而使用昂貴但較快速的SRAM技術。 廣義上的Cache指的是位於速度相差較大的兩種硬件之間, 用於協調兩者數據傳輸速度差異的結構。除了CPU與主存之間有Cache, 內存與硬盤之間也有Cache,乃至在硬盤與網絡之間也有某種意義上的Cache── 稱為Internet臨時文件夾或網絡內容緩存等。
Cache的容量有限,因此當Cache的容量用完后,而又有新的內容需要添加進來時, 就需要挑選並舍棄原有的部分內容,從而騰出空間來放新內容。LRU Cache 的替換原則就是將最近最少使用的內容替換掉。其實,LRU譯成最久未使用會更形象, 因為該算法每次替換掉的就是一段時間內最久沒有使用過的內容。
數據結構
LRU的典型實現是hash map + doubly linked list, 雙向鏈表用於存儲數據結點,並且它是按照結點最近被使用的時間來存儲的。 如果一個結點被訪問了, 我們有理由相信它在接下來的一段時間被訪問的概率要大於其它結點。於是, 我們把它放到雙向鏈表的頭部。當我們往雙向鏈表里插入一個結點, 我們也有可能很快就會使用到它,同樣把它插入到頭部。 我們使用這種方式不斷地調整着雙向鏈表,鏈表尾部的結點自然也就是最近一段時間, 最久沒有使用到的結點。那么,當我們的Cache滿了, 需要替換掉的就是雙向鏈表中最后的那個結點(不是尾結點,頭尾結點不存儲實際內容)。
如下是雙向鏈表示意圖,注意頭尾結點不存儲實際內容:
頭 --> 結 --> 結 --> 結 --> 尾 結 點 點 點 結 點 <-- 1 <-- 2 <-- 3 <-- 點
假如上圖Cache已滿了,我們要替換的就是結點3。
哈希表的作用是什么呢?如果沒有哈希表,我們要訪問某個結點,就需要順序地一個個找, 時間復雜度是O(n)。使用哈希表可以讓我們在O(1)的時間找到想要訪問的結點, 或者返回未找到。
Cache接口
當我們通過鍵值來訪問類型為T的數據時,調用Get函數。如果鍵值為key的數據已經在 Cache中,那就返回該數據,同時將存儲該數據的結點移到雙向鏈表頭部。 如果我們查詢的數據不在Cache中,我們就可以通過Put接口將數據插入雙向鏈表中。 如果此時的Cache還沒滿,那么我們將新結點插入到鏈表頭部, 同時用哈希表保存結點的鍵值及結點地址對。如果Cache已經滿了, 我們就將鏈表中的最后一個結點(注意不是尾結點)的內容替換為新內容, 然后移動到頭部,更新哈希表。
C++代碼
注意,hash map並不是C++標准的一部分,我使用的是Linux下g++ 4.6.1, hash_map放在/usr/include/c++/4.6/ext下,需要使用__gnu_cxx名空間, Linux平台可以切換到c++的include目錄:cd /usr/include/c++/版本 然后grep -iR “hash_map” ./ 查看在哪個文件中,一般頭文件的最后幾行會提示它所在的名空間。 當然如果你已經很fashion地在使用C++ 11,就不會有這些小困擾了。
#include <iostream>
#include <vector>
#include <hash_map>
using namespace std;
using namespace stdext;
template<class K, class T>
struct LRUCacheEntry
{
K key;
T data;
LRUCacheEntry* prev;
LRUCacheEntry* next;
};
template<class K, class T>
class LRUCache
{
private:
hash_map< K, LRUCacheEntry<K,T>* > _mapping;
vector< LRUCacheEntry<K,T>* > _freeEntries;// 存儲可用結點的地址
LRUCacheEntry<K,T> * head;
LRUCacheEntry<K,T> * tail;
LRUCacheEntry<K,T> * entries;// 雙向鏈表中的結點
public:
LRUCache(size_t size){
entries = new LRUCacheEntry<K,T>[size];
for (int i=0; i<size; i++)
_freeEntries.push_back(entries+i);// 存儲可用結點的地址
head = new LRUCacheEntry<K,T>;
tail = new LRUCacheEntry<K,T>;
head->prev = NULL;
head->next = tail;
tail->next = NULL;
tail->prev = head;
}
~LRUCache()
{
delete head;
delete tail;
delete [] entries;
}
void put(K key, T data)
{
LRUCacheEntry<K,T>* node = _mapping[key];
if(node)
{
// refresh the link list
detach(node);
node->data = data;
attach(node);
}
else{
if ( _freeEntries.empty() )
{// 可用結點為空,即cache已滿
node = tail->prev;
detach(node);
_mapping.erase(node->key);
node->data = data;
node->key = key;
attach(node);
}
else{
node = _freeEntries.back();
_freeEntries.pop_back();
node->key = key;
node->data = data;
_mapping[key] = node;
attach(node);
}
}
}
T get(K key)
{
LRUCacheEntry<K,T>* node = _mapping[key];
if(node)
{
detach(node);
attach(node);
return node->data;
}
else return NULL;// 如果cache中沒有,返回T的默認值。與hashmap行為一致
}
private:
// 分離結點
void detach(LRUCacheEntry<K,T>* node)
{
node->prev->next = node->next;
node->next->prev = node->prev;
}
// 將結點插入頭部
void attach(LRUCacheEntry<K,T>* node)
{
node->next = head->next;
node->prev = head;
head->next = node;
node->next->prev = node;
}
};
int main(){
hash_map<int, int> map;
map[9]= 999;
cout<<map[9]<<endl;
cout<<map[10]<<endl;
LRUCache<int, string> lru_cache(100);
lru_cache.Put(1, "one");
cout<<lru_cache.Get(1)<<endl;
if(lru_cache.Get(2) == "")
lru_cache.Put(2, "two");
cout<<lru_cache.Get(2);
return 0;
}
