1. 程式人生 > >如何用C++實現一個LRU Cache

如何用C++實現一個LRU Cache

什麼是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介面

Cache主要有兩個介面:

T Get(K key);
void Put
(K key, T data);

當我們通過鍵值來訪問型別為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” ./ 檢視在哪個檔案中,一般標頭檔案的最後幾行會提示它所在的名空間。 其它平臺請自行探索。XD

當然如果你已經很fashion地在使用C++ 11,就不會有這些小困擾了。

// A simple LRU cache written in C++
// Hash map + doubly linked list
#include <iostream>
#include <vector>
#include <ext/hash_map>
using namespace std;
using namespace __gnu_cxx;

template <class K, class T>
struct Node{
    K key;
    T data;
    Node *prev, *next;
};

template <class K, class T>
class LRUCache{
public:
    LRUCache(size_t size){
        entries_ = new Node<K,T>[size];
        for(int i=0; i<size; ++i)// 儲存可用結點的地址
            free_entries_.push_back(entries_+i);
        head_ = new Node<K,T>;
        tail_ = new Node<K,T>;
        head_->prev = NULL;
        head_->next = tail_;
        tail_->prev = head_;
        tail_->next = NULL;
    }
    ~LRUCache(){
        delete head_;
        delete tail_;
        delete[] entries_;
    }
    void Put(K key, T data){
        Node<K,T> *node = hashmap_[key];
        if(node){ // node exists
            detach(node);
            node->data = data;
            attach(node);
        }
        else{
            if(free_entries_.empty()){// 可用結點為空,即cache已滿
                node = tail_->prev;
                detach(node);
                hashmap_.erase(node->key);
            }
            else{
                node = free_entries_.back();
                free_entries_.pop_back();
            }
            node->key = key;
            node->data = data;
            hashmap_[key] = node;
            attach(node);
        }
    }
    T Get(K key){
        Node<K,T> *node = hashmap_[key];
        if(node){
            detach(node);
            attach(node);
            return node->data;
        }
        else{// 如果cache中沒有,返回T的預設值。與hashmap行為一致
            return T();
        }
    }
private:
    // 分離結點
    void detach(Node<K,T>* node){
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }
    // 將結點插入頭部
    void attach(Node<K,T>* node){
        node->prev = head_;
        node->next = head_->next;
        head_->next = node;
        node->next->prev = node;
    }
private:
    hash_map<K, Node<K,T>* > hashmap_;
    vector<Node<K,T>* > free_entries_; // 儲存可用結點的地址
    Node<K,T> *head_, *tail_;
    Node<K,T> *entries_; // 雙向連結串列中的結點
};

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;