为何LRU缓存比C++ unordered_map哈希表性能高出如此之多?
我对LRU缓存的了解仅停留在组成结构层面,但它相比普通哈希表的超高性能让我震惊。我在一个递归组合数学问题的动态规划实现中做了测试:分别用unordered_map哈希表和容量为1024的LRU缓存存储递归结果,运行时间从1秒骤降到0.006秒!这让我很困惑——哈希表多数操作时间复杂度是O(1),而LRU缓存还同时依赖哈希表和双向链表。
上下文信息:
- 用C++开发,测试中的哈希表是
<string, int>类型的unordered_map,我知道它最坏情况复杂度为N或N²,但通常操作都是O(1); - LRU缓存实现来自Stack Overflow。
带LRU缓存的代码
#include <bits/stdc++.h> using namespace std; using namespace std::chrono; template <typename T,typename U> std::pair<T,U> operator+(const std::pair<T,U> & l,const std::pair<T,U> & r) { return {l.first+r.first,l.second+r.second}; } #pragma GCC optimize ("Ofast") #pragma GCC target ("avx2") // LRU Cache implementation template <class KEY_T, class VAL_T> class LRUCache{ private: list< pair<KEY_T,VAL_T> > item_list; unordered_map<KEY_T, decltype(item_list.begin()) > item_map; size_t cache_size; private: void clean(void){ while(item_map.size()>cache_size){ auto last_it = item_list.end(); last_it --; item_map.erase(last_it->first); item_list.pop_back(); } }; public: LRUCache(int cache_size_):cache_size(cache_size_){ ; }; void put(const KEY_T &key, const VAL_T &val){ auto it = item_map.find(key); if(it != item_map.end()){ item_list.erase(it->second); item_map.erase(it); } item_list.push_front(make_pair(key,val)); item_map.insert(make_pair(key, item_list.begin())); clean(); }; bool exist(const KEY_T &key){ return (item_map.count(key)>0); }; VAL_T get(const KEY_T &key){ assert(exist(key)); auto it = item_map.find(key); item_list.splice(item_list.begin(), item_list, it->second); return it->second->second; }; }; // recursive solution to a combinatorics problem // number of permutations of each parcel int item_ways(int w, int n, int max_w){ if (w == 0 and n == 0) return 1; if (w <= 0 or n <= 0) return 0; int ways = 0; for (int i = 1; i <= max_w; i++) ways += item_ways(w-i, n-1, i); return ways; } // total combinations for answer LRUCache<string,int> dp(1024); //unordered_map<string,int> dp; int parcel_ways(int p, int max_w, int n, int w){ if (p == 0 and n == 0) return 1; if (p <= 0 and n <= 0) return 0; string x; x += char(p); x += char(max_w); x += char(n); x += char(w); if(dp.exist(x)) // caching/dp skips recursion here { return dp.get(x); } int ways = 0; for (int i = 1; i <= n; i++){ ways += parcel_ways(p-1, max_w, n-i, w) * item_ways(w, i, max_w); } dp.put(x,ways); // cache here return ways; } // input any 4 numbers for problem void solve() { auto start = high_resolution_clock::now(); cout << parcel_ways(5,8,23,17); auto stop = high_resolution_clock::now(); auto duration = duration_cast<microseconds>(stop - start); cout << "Time taken by function: " << duration.count() << " microseconds" << endl; } int main() { solve(); return 0; }
带unordered_map(哈希表)的代码
#include <bits/stdc++.h> using namespace std; using namespace std::chrono; template <typename T,typename U> std::pair<T,U> operator+(const std::pair<T,U> & l,const std::pair<T,U> & r) { return {l.first+r.first,l.second+r.second}; } #pragma GCC optimize ("Ofast") #pragma GCC target ("avx2") // number of permutations of each parcel int item_ways(int w, int n, int max_w){ if (w == 0 and n == 0) return 1; if (w <= 0 or n <= 0) return 0; int ways = 0; for (int i = 1; i <= max_w; i++) ways += item_ways(w-i, n-1, i); return ways; } // total combinations for answer unordered_map<string,int> dp; int parcel_ways(int p, int max_w, int n, int w){ if (p == 0 and n == 0) return 1; if (p <= 0 and n <= 0) return 0; string x; x += char(p); x += char(max_w); x += char(n); x += char(w); if(dp[x]) // caching/dp skips recursion here { return dp[x]; } int ways = 0; for (int i = 1; i <= n; i++){ ways += parcel_ways(p-1, max_w, n-i, w) * item_ways(w, i, max_w); } dp[x] = ways; // cache here return ways; } void solve() { auto start = high_resolution_clock::now(); cout << parcel_ways(5,8,23,17); auto stop = high_resolution_clock::now(); auto duration = duration_cast<microseconds>(stop - start); cout << "Time taken by function: " << duration.count() << " microseconds" << endl; } int main() { solve(); return 0; }
这个性能差距的核心原因不是LRU本身的结构,而是两个实现中缓存命中检查的行为差异,以及unordered_map的隐藏陷阱:
unordered_map的operator[]触发无效插入
你在unordered_map版本中用if(dp[x])检查缓存是否存在——这会调用unordered_map的operator[],而这个操作的特性是:如果键x不存在,会自动插入一个值为0的键值对。这导致你的哈希表中塞满了大量从未被实际计算、使用的无效键值对,负载因子急剧上升,哈希冲突概率大幅增加,实际操作的时间复杂度从理论O(1)退化成接近O(n),还会引发频繁的哈希表扩容(扩容需要重新哈希所有元素,开销极大)。LRU的
exist()是纯查询操作
你使用的LRU缓存实现中,exist()调用的是item_map.count(key),这是纯查询行为,不会插入任何元素。只有当状态被实际计算出来后,才会通过put()加入缓存。因此LRU缓存中的元素全是有效的,哈希表负载低、冲突少,操作始终保持接近O(1)的性能。LRU容量限制优化了内存局部性
你的LRU缓存容量为1024,而unordered_map会无限制膨胀。较小的缓存大小意味着缓存元素更可能被CPU的L1/L2缓存命中,内存访问速度远快于unordered_map膨胀后的冷内存访问,进一步放大了性能差距。
验证方法
把unordered_map版本的检查代码改成以下形式,避免触发默认插入:
if (dp.find(x) != dp.end()) { return dp[x]; }
修改后你会发现unordered_map的性能会大幅提升,和LRU版本的差距会显著缩小。
内容的提问来源于stack exchange,提问作者ron0studios

