緩存淘汰算法 LRU 和 LFU


LRU (Least Recently Used), 即最近最少使用算法,是一種常見的 Cache 頁面置換算法,有利於提高 Cache 命中率。

LRU 的算法思想:對於每個頁面,記錄該頁面自上一次被訪問以來所經歷的時間 \(t\),當淘汰一個頁面時,應選擇所有頁面中其 \(t\) 值最大的頁面,即內存中最近一段時間內最長時間未被使用的頁面予以淘汰。

LFU (Least Frequently Used) :為每個頁面配置一個計數器,一旦某頁被訪問,則將其計數器的值加1,在需要選擇一頁置換時,則將選擇其計數器值最小的頁面,即內存中訪問次數最少的頁面進行淘汰。

其余常見的頁面置換算法還有:

  • OPT:理想化的置換算法,假設 OS 知道程序后續訪問的所有頁面的順序,每次淘汰頁面時,OPT 選擇的頁面將是以后永不使用的,或是在最長(未來)時間內不再被訪問的頁面。采用 OPT 通常可保證最低的缺頁率(最高的 Cache 命中率)。
  • FIFO:先進先出
  • Clock 置換,又稱最近未用算法 (NRU,Not Recently Used) 。
    • 算法思想:為每個頁面設置一個訪問位,再將內存中的頁面都通過鏈接指針鏈接成一個環形隊列。當某個頁被訪問時,其訪問位置 1。當需要淘汰一個頁面時,只需檢查頁的訪問位。如果是 0,就選擇該頁換出;如果是 1,暫不換出,將訪問位改為 0,繼續檢查下一個頁面。若第一輪掃描中所有的頁面都是 1,則將這些頁面的訪問位一次置為 0 后,再進行第二輪掃描,第二輪掃描中一定會有訪問位為 0 的頁面。

LRU

例子

給定一個程序的頁面訪問序列:7 0 1 2 0 3 0 4,假設實際 Cache 只有 3 個頁面大小,根據 LRU,畫出 Cache 中的頁面變化過程。

使用一個棧(大小是 3),棧頂總是最新訪問的頁面。當棧滿時,最新訪問頁面為 x

  • x 不在棧當中,去除棧底元素,把 x 置入棧頂。
  • x 在棧當中,把 x 移至棧頂,其他頁面順序不變。

如下圖所示,圖源自知乎

如果簡單使用一個數組來模擬上述過程,訪問一次頁面的平均時間復雜度為 \(O(n)\)

實現

在這里,LRU 存放的數據是一個鍵值對 (key, val)

Leetcode 題目:146. LRU 緩存機制

要求 getput 操作都在 \(O(1)\) 時間內完成。

方法:雙向鏈表+哈希。雙向鏈表頭尾各自帶有一個 dummy 節點(可以簡化插入、刪除操作的代碼)。

哈希表與鏈表的關系如圖所示(圖來源自 Leetcode 討論區)。

需要保證 get 方法在 \(O(1)\) 內完成,因此哈希表只能使用 unordered_map 而不是 map

struct Node
{
    int key, value;
    Node *next, *prev;
    Node(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
class List
{
public:
    Node *head, *tail;
    int length;
    List() : length(0)
    {
        head = new Node(-1, -1), tail = new Node(-1, -1);
        head->next = tail, tail->prev = head;
    }
    void pushFront(Node *node)
    {
        auto next = head->next;
        head->next = node, node->next = next;
        next->prev = node, node->prev = head;
        length++;
    }
    void pushBack(Node *node)
    {
        auto prev = tail->prev;
        prev->next = node, node->next = tail;
        tail->prev = node, node->prev = prev;
        length++;
    }
    bool empty() { return length == 0 && head->next == tail && tail->prev == head; }
    Node *popFront() { return remove(head->next); }
    Node *popBack() { return remove(tail->prev); }
    Node *remove(Node *node)
    {
        if (node == nullptr || empty()) return nullptr;
        auto prev = node->prev, next = node->next;
        prev->next = next, next->prev = prev;
        node->next = node->prev = nullptr;
        length--;
        return node;
    }
};
class LRUCache
{
public:
    unordered_map<int, Node *> table;
    List list;
    int capacity;
    LRUCache(int capacity) { this->capacity = capacity; }

    int get(int key)
    {
        if (table.count(key) == 0) return -1;
        else
        {
            auto node = list.remove(table[key]);
            list.pushFront(node);
            return node->value;
        }
    }

    void put(int key, int value)
    {
        if (table.count(key) == 0)
        {
            auto node = new Node(key, value);
            table[key] = node;
            if (list.length == capacity)
            {
                auto p = list.popBack();
                table.erase(p->key);
                delete p;
            }
            list.pushFront(node);
        }
        else
        {
            table[key]->value = value;
            list.pushFront(list.remove(table[key]));
        }
    }
};

下面嘗試使用 STL 中的 list 完成。

list 中,調用 push, emplace, pop 等操作,不會引起其他節點的迭代器失效。

struct node
{
    int key, value;
    node(int k, int v) : key(k), value(v) {}
};
class LRUCache
{
public:
    unordered_map<int, list<node>::iterator> table;
    list<node> data;
    int capacity, length;
    LRUCache(int capacity) : length(0) { this->capacity = capacity; }

    int get(int key)
    {
        if (table.count(key) == 0) return -1;
        else
        {
            auto itor = table[key];
            int key = itor->key, val = itor->value;
            data.erase(itor);
            data.emplace_front(key, val);
            table[key] = data.begin();
            return val;
        }
    }

    void put(int key, int value)
    {
        if (table.count(key) == 0)
        {
            if (length == capacity)
            {
                auto itor = data.rbegin();
                table.erase(itor->key);
                data.pop_back();
                length--;
            }
            data.emplace_front(key, value);
            table[key] = data.begin();
            length++;
        }
        else
        {
            auto itor = table[key];
            data.erase(itor);
            data.emplace_front(key, value);
            table[key] = data.begin();
        }
    }
};

LFU

Leetcode 題目:460. LFU 緩存

LFU (Least Frequently Used) 的主要思想是為每個緩存項一個計數器,一旦某個緩存項被訪問,則將其計數器的值加 1,在需要淘汰緩存項時,則將選擇其計數器值最小的,即內存中訪問次數最少的緩存進行淘汰。

按照這一描述,首先想到的是可以通過哈希表 + 優先隊列(小頂堆),堆中的數據以緩存項的計數器作為鍵值排序

對於 get 方法而言,通過哈希表找到 key 對應元素的位置,計數器加 1,重新調整堆,時間復雜度為 \(O(\log{n})\) .

對於 put 方法而言,如果緩存中存在該元素,計數器加一,重新調整堆,時間復雜度為 \(O(\log{n})\) ;如果不存在該元素並且緩存已滿,那么直接把堆頂元素替換為新的元素 <key,value>(因為新元素的計數器為 1 ,必然也是最小的),時間復雜度為 \(O(1)\)。因此,put 方法總的時間復雜度為 \(O(\log{n})\) .

這一方法基於堆實現,無法保證當 2 個元素的計數器相同時,被淘汰的是「較舊」的元素。

哈希/鏈表

現考慮 getput 均為 \(O(1)\) 的解法。

考慮基於「哈希+十字鏈表」實現,如下圖所示(出處見水印)。

鏈表節點定義為:

struct Node
{
    int key, value, counter;
    Node *prev, *next, *another;
};

hashmap 用於記錄 key -> Node* 的映射關系,輔助 get 方法在 \(O(1)\) 時間內完成。

對於訪問次數相同的節點,用鏈表連接起來(上圖的橫向鏈表),鏈表的頭部記錄訪問次數,隨后連接緩存數據的節點,數據節點有一個額外的指針 another 指向第一個節點(即記錄訪問次數的節點),然后把所有鏈表的頭部也連接起來(上圖的縱向鏈表),形成十字鏈表。

對於 get 方法,通過 p = hashmap[key] 找到指向數據的指針,然后把 p 移動到 count+1 的鏈表尾部(如果鏈表 count+1 不存在則插入一個)。

考慮 put 方法的最壞情況,如果 <key, val> 不在緩存當中, 並且緩存已滿,那么就從十字鏈表的第一個鏈表(count 值最小的鏈表)刪除尾部節點,並在頭部插入新節點(這么做是為了按照從新到舊存儲每個數據,尾部就是「最舊」的節點,可以做到計數相同的情況下,淘汰舊元素)。

但這種「十字鏈表」結構實現起來過於復雜(代碼肯定不簡潔),所以我們把「十字鏈表」轉換為一個哈希表 hash<int, List> ,如下圖所示。

代碼實現:

struct Node
{
    int key, value, counter;
    Node *next, *prev;
    Node(int k, int v) : key(k), value(v), counter(1), prev(nullptr), next(nullptr) {}
};
class List
{
public:
    Node *head, *tail;
    int length;
    List() : length(0)
    {
        head = new Node(-1, -1), tail = new Node(-1, -1);
        head->next = tail, tail->prev = head;
    }
    void pushFront(Node *node)
    {
        auto next = head->next;
        head->next = node, node->next = next;
        next->prev = node, node->prev = head;
        length++;
    }
    void pushBack(Node *node)
    {
        auto prev = tail->prev;
        prev->next = node, node->next = tail;
        tail->prev = node, node->prev = prev;
        length++;
    }
    bool empty() { return length == 0 && head->next == tail && tail->prev == head; }
    Node *popFront() { return remove(head->next); }
    Node *popBack() { return remove(tail->prev); }
    Node *remove(Node *node)
    {
        if (node == nullptr || empty()) return nullptr;
        auto prev = node->prev, next = node->next;
        prev->next = next, next->prev = prev;
        node->next = node->prev = nullptr;
        length--;
        return node;
    }
};
class LFUCache
{
public:
    unordered_map<int, Node *> table;
    unordered_map<int, List> data;
    int capacity, total, minCounter;
    LFUCache(int capacity)
    {
        this->capacity = capacity;
        this->total = 0;
        this->minCounter = 1;
    }

    int get(int key)
    {
        if (table.count(key) == 0) return -1;
        else
        {
            auto node = table[key];
            int counter = node->counter;
            node->counter++;
            data[counter].remove(node);
            data[counter + 1].pushFront(node);
            if (data[counter].length == 0 && counter == minCounter)
                minCounter = counter + 1;
            return node->value;
        }
    }

    void put(int key, int value)
    {
        if (capacity == 0) return;
        if (table.count(key) == 0)
        {
            auto node = new Node(key, value);
            if (total == capacity)
            {
                auto p = data[minCounter].popBack();
                table.erase(p->key);
                total--;
                delete p;
            }
            minCounter = 1;
            table[key] = node;
            data[node->counter].pushFront(node);
            total++;
        }
        else
        {
            auto node = table[key];
            int counter = node->counter;
            node->value = value, node->counter++;
            data[counter].remove(node);
            data[counter + 1].pushFront(node);
            if (data[counter].length == 0 && counter == minCounter)
                minCounter = counter + 1;
        }
    }
};

基於 STL 的 list 實現。

(最近老是因為手殘而 de 一些毫無意義的 bug,真的服了自己,這里就因為把 node 的構造函數初始化 counter(c) 寫成為 counter(1),白白糾結了 1 個多小時)

struct node
{
    int key, value, counter;
    node(int k, int v, int c = 1) : key(k), value(v), counter(c) {}
};
class LFUCache
{
public:
    unordered_map<int, list<node>::iterator> table;
    unordered_map<int, list<node>> data;
    int capacity, size, mincounter;
    LFUCache(int capacity) : size(0), mincounter(1)
    { this->capacity = capacity; }

    int get(int key)
    {
        if (table.count(key))
        {
            auto itor = table[key];
            int k = itor->key, v = itor->value, c = itor->counter;
            data[c].erase(itor);
            data[c + 1].emplace_front(k, v, c + 1);
            table[key] = data[c + 1].begin();
            if (data[c].size() == 0 && mincounter == c)
                mincounter = c + 1;
            return v;
        }
        return -1;
    }

    void put(int key, int value)
    {
        if (capacity == 0) return;
        if (table.count(key) == 0)
        {
            if (size == capacity)
            {
                auto node = data[mincounter].back();
                table.erase(node.key);
                data[mincounter].pop_back();
                size--;
            }
            mincounter = 1;
            data[1].emplace_front(key, value);
            table[key] = data[1].begin();
            size++;
        }
        else
        {
            auto itor = table[key];
            int counter = itor->counter;
            data[counter].erase(itor);
            data[counter + 1].emplace_front(key, value, counter + 1);
            table[key] = data[counter + 1].begin();
            if (data[counter].size() == 0 && mincounter == counter)
                mincounter = counter + 1;
        }
    }
};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM