快取淘汰演算法 LRU 和 LFU

2022-01-09 21:11:18 字數 4247 閱讀 4835

lru (least recently used), 即最近最少使用演算法,是一種常見的 cache 頁面置換演算法,有利於提高 cache 命中率。

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

lfu (least frequently used) :為每個頁面配置乙個計數器,一旦某頁被訪問,則將其計數器的值加1,在需要選擇一頁置換時,則將選擇其計數器值最小的頁面,即記憶體中訪問次數最少的頁面進行淘汰。

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

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

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

如下圖所示,圖源自知乎。

如果簡單使用乙個陣列來模擬上述過程,訪問一次頁面的平均時間複雜度為 \(o(n)\) 。

在這裡,lru 存放的資料是乙個鍵值對(key, val)

leetcode 題目:146. lru 快取機制。

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

方法:雙向鍊錶+雜湊。雙向鍊錶頭尾各自帶有乙個dummy節點(可以簡化插入、刪除操作的**)。

雜湊表與鍊錶的關係如圖所示(圖**自 leetcode 討論區)。

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

struct node

};class list

void pushfront(node *node)

void pushback(node *node)

bool empty()

node *popfront()

node *popback()

node *remove(node *node)

};class lrucache

int get(int key)

}void put(int key, int value)

list.pushfront(node);

}else}};

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

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

struct node

};class lrucache

int get(int key)

}void put(int key, int value)

data.emplace_front(key, value);

table[key] = data.begin();

length++;

}else}};

leetcode 題目:460. lfu 快取。

lfu (least frequently used) 的主要思想是為每個快取項乙個計數器,一旦某個快取項被訪問,則將其計數器的值加 1,在需要淘汰快取項時,則將選擇其計數器值最小的,即記憶體中訪問次數最少的快取進行淘汰。

按照這一描述,首先想到的是可以通過雜湊表 + 優先佇列(小頂堆),堆中的資料以快取項的計數器作為鍵值排序

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

對於put方法而言,如果快取中存在該元素,計數器加一,重新調整堆,時間複雜度為 \(o(\log)\) ;如果不存在該元素並且快取已滿,那麼直接把堆頂元素替換為新的元素(因為新元素的計數器為 1 ,必然也是最小的),時間複雜度為 \(o(1)\)。因此,put方法總的時間複雜度為 \(o(\log)\) .

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

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

考慮基於「雜湊+十字鍊錶」實現,如下圖所示(出處見水印)。

鍊錶節點定義為:

hashmap用於記錄key -> node*的對映關係,輔助get方法在 \(o(1)\) 時間內完成。

對於訪問次數相同的節點,用鍊錶連線起來(上圖的橫向鍊錶),鍊錶的頭部記錄訪問次數,隨後連線快取資料的節點,資料節點有乙個額外的指標another指向第乙個節點(即記錄訪問次數的節點),然後把所有鍊錶的頭部也連線起來(上圖的縱向鍊錶),形成十字鍊錶。

對於get方法,通過p = hashmap[key]找到指向資料的指標,然後把p移動到count+1的鍊錶尾部(如果鍊錶count+1不存在則插入乙個)。

考慮put方法的最壞情況,如果不在快取當中, 並且快取已滿,那麼就從十字鍊錶的第乙個鍊錶(count值最小的鍊錶)刪除尾部節點,並在頭部插入新節點(這麼做是為了按照從新到舊儲存每個資料,尾部就是「最舊」的節點,可以做到計數相同的情況下,淘汰舊元素)。

但這種「十字鍊錶」結構實現起來過於複雜(**肯定不簡潔),所以我們把「十字鍊錶」轉換為乙個雜湊表hash,如下圖所示。

**實現:

struct node

};class list

void pushfront(node *node)

void pushback(node *node)

bool empty()

node *popfront()

node *popback()

node *remove(node *node)

};class lfucache

int get(int key)

}void put(int key, int value)

mincounter = 1;

table[key] = node;

data[node->counter].pushfront(node);

total++;

}else}};

基於 stl 的list實現。

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

struct node

};class lfucache

int get(int key)

return -1;

}void put(int key, int value)

mincounter = 1;

data[1].emplace_front(key, value);

table[key] = data[1].begin();

size++;

}else}};

快取淘汰演算法 LRU

1.lru 1.1.原理 lru least recently used,最近最少使用 演算法根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是 如果資料最近被訪問過,那麼將來被訪問的機率也更高 1.2.實現 最常見的實現是使用乙個鍊錶儲存快取資料,詳細演算法實現如下 1.新資料插入到鍊錶頭部 2....

LRU 快取淘汰演算法

1.介紹 lru是leastrecentlyused近期最少使用演算法。記憶體管理的一種頁面置換演算法,對於在記憶體中但又不用的資料塊 記憶體塊 叫做lru,oracle會根據哪些資料屬於lru而將其移出記憶體而騰出空間來載入另外的資料。lru least recently used,最近最少使用 ...

LRU快取淘汰演算法

這是乙個什麼演算法?這是乙個可以處理程式過多的情況下該刪除哪乙個程式的演算法策略。它是根據最近使用時間來進行確定的,通常刪除的是最後乙個節點。那麼這個演算法會涉及什麼樣的資料結構?這個演算法涉及了hashmap和雙向鍊錶的資料結構,通過這兩個結構的配合可以通過map來快速定位訪問節點,通過雙向鍊錶來...