1. 程式人生 > >設計並實現一個LRU Cache

設計並實現一個LRU Cache

一、什麼是Cache

1 概念

Cache,即快取記憶體,是介於CPU和記憶體之間的高速小容量儲存器。在金字塔式儲存體系中它位於自頂向下的第二層,僅次於CPU暫存器。其容量遠小於記憶體,但速度卻可以接近CPU的頻率。

當CPU發出記憶體訪問請求時,會先檢視 Cache 內是否有請求資料。

  • 如果存在(命中),則直接返回該資料;
  • 如果不存在(失效),再去訪問記憶體 —— 先把記憶體中的相應資料載入快取,再將其返回處理器。

提供“快取記憶體”的目的是讓資料訪問的速度適應CPU的處理速度,通過減少訪問記憶體的次數來提高資料存取的速度。

2 原理

Cache 技術所依賴的原理是”程式執行與資料訪問的區域性性原理

“,這種區域性性表現在兩個方面:

  1. 時間區域性性:如果程式中的某條指令一旦執行,不久以後該指令可能再次執行,如果某資料被訪問過,不久以後該資料可能再次被訪問。
  2. 空間區域性性:一旦程式訪問了某個儲存單元,在不久之後,其附近的儲存單元也將被訪問,即程式在一段時間內所訪問的地址,可能集中在一定的範圍之內,這是因為指令或資料通常是順序存放的。

時間區域性性是通過將近來使用的指令和資料儲存到Cache中實現。空間區域性性通常是使用較大的快取記憶體,並將 預取機制 整合到快取記憶體控制邏輯中來實現。

3 替換策略

Cache的容量是有限的,當Cache的空間都被佔滿後,如果再次發生快取失效,就必須選擇一個快取塊來替換掉。常用的替換策略有以下幾種:

  1. 隨機演算法(Rand):隨機法是隨機地確定替換的儲存塊。設定一個隨機數產生器,依據所產生的隨機數,確定替換塊。這種方法簡單、易於實現,但命中率比較低。

  2. 先進先出演算法(FIFO, First In First Out):先進先出法是選擇那個最先調入的那個塊進行替換。當最先調入並被多次命中的塊,很可能被優先替換,因而不符合區域性性規律。這種方法的命中率比隨機法好些,但還不滿足要求。

  3. 最久未使用演算法(LRU, Least Recently Used):LRU法是依據各塊使用的情況, 總是選擇那個最長時間未被使用的塊替換。這種方法比較好地反映了程式區域性性規律。

  4. 最不經常使用演算法(LFU, Least Frequently Used)

    :將最近一段時期內,訪問次數最少的塊替換出Cache。

4 概念的擴充

如今快取記憶體的概念已被擴充,不僅在CPU和主記憶體之間有Cache,而且在記憶體和硬碟之間也有Cache(磁碟快取),乃至在硬碟與網路之間也有某種意義上的Cache──稱為Internet臨時資料夾或網路內容快取等。凡是位於速度相差較大的兩種硬體之間,用於協調兩者資料傳輸速度差異的結構,均可稱之為Cache。

二、LRU Cache的實現

Google的一道面試題:

Design an LRU cache with all the operations to be done in O(1) .

1 思路分析

對一個Cache的操作無非三種:插入(insert)、替換(replace)、查詢(lookup)。

為了能夠快速刪除最久沒有訪問的資料項和插入最新的資料項,我們使用 雙向連結串列 連線Cache中的資料項,並且保證連結串列維持資料項從最近訪問到最舊訪問的順序。

  • 插入:當Cache未滿時,新的資料項只需插到雙鏈表頭部即可。時間複雜度為O(1).

  • 替換:當Cache已滿時,將新的資料項插到雙鏈表頭部,並刪除雙鏈表的尾結點即可。時間複雜度為O(1).

  • 查詢:每次資料項被查詢到時,都將此資料項移動到連結串列頭部。

經過分析,我們知道使用雙向連結串列可以保證插入和替換的時間複雜度是O(1),但查詢的時間複雜度是O(n),因為需要對雙鏈表進行遍歷。為了讓查詢效率也達到O(1),很自然的會想到使用 hash table

2 程式碼實現

從上述分析可知,我們需要使用兩種資料結構:

  1. 雙向連結串列(Doubly Linked List)
  2. 雜湊表(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;
}