c++ - LRU 缓存如何比 HashMap 更快?

标签 c++ algorithm data-structures dynamic-programming computer-science

除了 LRU 缓存由哪些结构构成之外,我并没有深入了解它,但我仍然对它比常规 hashmap 快得多感到惊讶。

我做了一个测试,一个递归组合问题,使用常规 HashMap 来保存递归(动态规划)期间的结果结果,并且做了同样的事情,唯一的区别是使用了 LRU 缓存实现(大小 1024)相反。

性能从 1 秒下降到 0.006 秒!

现在,这非常令人惊讶,我不知道为什么会这样。 HashMap 对于大多数操作具有 O(1) 时间复杂度,LRU 缓存需要 HashMap 双向链表。

上下文:

  • 我在这个项目中使用 C++。有问题的 hashmap 是一个 unordered_map,以字符串为键,以整数为值。我听说过 unordered_map 的最坏情况复杂度为 NN2,但据我所知,它通常在 O(1) 中执行所有操作。

  • LRU 缓存实现是从堆栈溢出中复制粘贴的 :D

代码

使用 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;
}

最佳答案

在您的实现中有很多事情不是最佳的,但我看到的唯一可以使您看到的差异幅度很大的是:

if(dp[x]) // caching/dp skips recursion here
{
    return dp[x];
}

如果 dp[x]==0 则不会返回,因此您将重新计算任何 0 结果。

带有LRU缓存的版本使用exists,在这种情况下会提前返回。

这可以通过使用 dp.contains(x) (或者 dp.count(x) 如果你没有 c++20)

关于c++ - LRU 缓存如何比 HashMap 更快?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73222750/

相关文章:

algorithm - 计算被两条垂直线切割的水平线段

algorithm - 有效地从两个文件中找到所有常见模式(子字符串)

data-structures - 二叉堆优先队列的位置索引?

c++ - 链表与哈希表中的开放寻址

c++ - _BLOCK_TYPE_IS_VALID(pHead->nBlockUse) 在 "return 0"之后

c++ - typedef 用于可以包含 size_t 的有符号类型?

python - 置换计算运行时复杂度有一些变化

c++ - 局部常量变量不是 constexpr 可评估的,但无法弄清楚为什么

c++ - 数据包延迟变化 (PDV)

algorithm - 找出是否有可能用一些给定的数字组成一个整数