Cache,即高速緩存,是介於CPU和內存之間的高速小容量存儲器。在金字塔式存儲體系中它位於自頂向下的第二層,僅次於CPU寄存器。其容量遠小於內存,但速度卻能夠接近CPU的頻率。node
當CPU發出內存訪問請求時,會先查看 Cache 內是否有請求數據。ios
若是存在(命中),則直接返回該數據;面試
若是不存在(失效),再去訪問內存 —— 先把內存中的相應數據載入緩存,再將其返回處理器。算法
提供「高速緩存」的目的是讓數據訪問的速度適應CPU的處理速度,經過減小訪問內存的次數來提升數據存取的速度。緩存
Cache 技術所依賴的原理是」程序執行與數據訪問的局部性原理「,這種局部性表如今兩個方面:網絡
時間局部性:若是程序中的某條指令一旦執行,不久之後該指令可能再次執行,若是某數據被訪問過,不久之後該數據可能再次被訪問。數據結構
空間局部性:一旦程序訪問了某個存儲單元,在不久以後,其附近的存儲單元也將被訪問,即程序在一段時間內所訪問的地址,可能集中在必定的範圍以內,這是由於指令或數據一般是順序存放的。函數
時間局部性是經過將近來使用的指令和數據保存到Cache中實現。空間局部性一般是使用較大的高速緩存,並將 預取機制 集成到高速緩存控制邏輯中來實現。this
Cache的容量是有限的,當Cache的空間都被佔滿後,若是再次發生緩存失效,就必須選擇一個緩存塊來替換掉。經常使用的替換策略有如下幾種:spa
隨機算法(Rand):隨機法是隨機地肯定替換的存儲塊。設置一個隨機數產生器,依據所產生的隨機數,肯定替換塊。這種方法簡單、易於實現,但命中率比較低。
先進先出算法(FIFO, First In First Out):先進先出法是選擇那個最早調入的那個塊進行替換。當最早調入並被屢次命中的塊,極可能被優先替換,於是不符合局部性規律。這種方法的命中率比隨機法好些,但還不知足要求。
最久未使用算法(LRU, Least Recently Used):LRU法是依據各塊使用的狀況, 老是選擇那個最長時間未被使用的塊替換。這種方法比較好地反映了程序局部性規律。
最不常用算法(LFU, Least Frequently Used):將最近一段時期內,訪問次數最少的塊替換出Cache。
現在高速緩存的概念已被擴充,不只在CPU和主內存之間有Cache,並且在內存和硬盤之間也有Cache(磁盤緩存),乃至在硬盤與網絡之間也有某種意義上的Cache──稱爲Internet臨時文件夾或網絡內容緩存等。凡是位於速度相差較大的兩種硬件之間,用於協調二者數據傳輸速度差別的結構,都可稱之爲Cache。
Google的一道面試題:
Design an LRU cache with all the operations to be done in O(1) .
對一個Cache的操做無非三種:插入(insert)、替換(replace)、查找(lookup)。
爲了可以快速刪除最久沒有訪問的數據項和插入最新的數據項,咱們使用 雙向鏈表 鏈接Cache中的數據項,而且保證鏈表維持數據項從最近訪問到最舊訪問的順序。
插入:當Cache未滿時,新的數據項只需插到雙鏈表頭部便可。時間複雜度爲O(1).
替換:當Cache已滿時,將新的數據項插到雙鏈表頭部,並刪除雙鏈表的尾結點便可。時間複雜度爲O(1).
查找:每次數據項被查詢到時,都將此數據項移動到鏈表頭部。
通過分析,咱們知道使用雙向鏈表能夠保證插入和替換的時間複雜度是O(1),但查詢的時間複雜度是O(n),由於須要對雙鏈表進行遍歷。爲了讓查找效率也達到O(1),很天然的會想到使用 hash table 。
從上述分析可知,咱們須要使用兩種數據結構:
雙向鏈表(Doubly Linked List)
哈希表(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; }