C++ LRU 缓存 - 需要有关如何提高速度的建议

Posted

技术标签:

【中文标题】C++ LRU 缓存 - 需要有关如何提高速度的建议【英文标题】:C++ LRU cache - need suggestions on how to improve speed 【发布时间】:2020-04-27 04:48:21 【问题描述】:

任务是实现一个 O(1) 最近最少使用的缓存

这是关于leetcode的问题https://leetcode.com/problems/lru-cache/

这是我的解决方案,虽然它是 O(1),但它不是最快的实现您能否提供一些反馈,或者关于如何优化它的想法?谢谢!


#include<unordered_map>
#include<list>

class LRUCache 

    // umap<key,<value,listiterator>>
    // store the key,value, position in list(iterator) where push_back occurred
private:
    unordered_map<int,pair<int,list<int>::iterator>> umap;
    list<int> klist;  
    int cap = -1;

public:
    LRUCache(int capacity):cap(capacity)

    

    int get(int key) 
        // if the key exists in the unordered map
        if(umap.count(key))
            // remove it from the old position 
            klist.erase(umap[key].second);
            klist.push_back(key);
            list<int>::iterator key_loc = klist.end();
            umap[key].second = --key_loc;
            return umap[key].first;
        
        return -1;
    

    void put(int key, int value) 

        // if key already exists delete it from the the umap and klist
        if(umap.count(key))
            klist.erase(umap[key].second);
            umap.erase(key);
        
        // if the unordered map is at max capacity
        if(umap.size() == cap)
            umap.erase(klist.front());
            klist.pop_front();
        
        // finally update klist and umap
        klist.push_back(key);
        list<int>::iterator key_loc = klist.end();
        umap[key].first = value;
        umap[key].second = --key_loc;
        return;
    
;

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache* obj = new LRUCache(capacity);
 * int param_1 = obj->get(key);
 * obj->put(key,value);
 */

【问题讨论】:

从技术上讲,地图大小不应超过容量,否则这是一个错误的设计 从实际操作系统的角度来看是有意义的。 【参考方案1】:

以下一些优化可能会有所帮助:

get函数中获取这段代码:

    if(umap.count(key))
        // remove it from the old position 
        klist.erase(umap[key].second);

上面会在地图中查找key两次。一次用于count 方法,看看它是否存在。另一个调用[] 运算符来获取它的值。这样做可以节省几个周期:

auto itor = umap.find(key);
if (itor != umap.end()) 
        // remove it from the old position 
        klist.erase(itor->second);

put 函数中,您可以这样做:

    if(umap.count(key))
        klist.erase(umap[key].second);
        umap.erase(key);
    

get,可以通过umap避免冗余搜索。此外,没有理由调用 umap.erase 只是为了在几行之后将相同的键添加回映射中。

另外,这也是低效的

    umap[key].first = value;
    umap[key].second = --key_loc;

与上面类似,在地图中重复查找 key 两次。在第一个赋值语句中,key 不在映射中,因此它默认构造一个新的值对事物。第二个任务是在地图中进行另一次查找。

让我们重组你的put函数如下:

void put(int key, int value) 

    auto itor = umap.find(key);
    bool reinsert = (itor != umap.end());

    // if key already exists delete it from the klist only
    if (reinsert) 
        klist.erase(umap[key].second);
    
    else 
        // if the unordered map is at max capacity
        if (umap.size() == cap) 
            umap.erase(klist.front());
            klist.pop_front();
        
    

    // finally update klist and umap
    klist.push_back(key);
    list<int>::iterator key_loc = klist.end();
    auto endOfList = --key_loc;

    if (reinsert) 
        itor->second.first = value;
        itor->second.second = endOfList;
    
    else  
        const pair<int, list<int>::iterator> itempair =  value, endOfList ;
        umap.emplace(key, itempair);
    

这可能是您使用std::list 所能达到的程度。 list 类型的缺点是,如果不先将现有节点删除然后再将其添加回来,就无法将现有节点从中间移动到前面(或后面)。这是更新列表的几个不需要的内存分配。可能的替代方法是您只需使用自己的双链表类型并自己手动修复 prev/next 指针。

【讨论】:

您可以使用std::list::splice 将同一列表中的节点移动到前面或后面。 @selbie 谢谢!!我尝试了您的建议,尽管代码改进了它现在运行时间为 172 毫秒而不是 184 毫秒,而且 umap.emplace(key,itempair) 与 umap[key] = itempair 有何不同。据我了解 unordered_map 插入和查找是 O(1) 。 @Blastfurnace 尝试使用 klist.splice(klist.end(),klist,it->second.second);给我 [ AddressSanitizer : alloc-dealloc-mismatch。 ] 这是将中间节点移到列表末尾的正确方法吗? @auxeon 看起来是正确的。除了this is the code 我在 LeetCode 上使用并被接受之外,我不在一个可以测试任何东西的地方。请注意,我正在拼接到前面,但拼接到后面也应该可以。【参考方案2】:

这是我的解决方案,虽然它是 O(1),但它不是最快的实现 你能提供一些反馈,也许我该如何优化这个想法?谢谢!

我要在这里讨论 selbie 的观点:if(umap.count(key)) 的每个实例都将搜索键,使用 umap[key] 是搜索的等效项。您可以通过单个 std::unordered_map::find() 操作分配一个指向键的迭代器来避免重复搜索。

selbie 已经给出了int get() 的搜索代码,这是void put() 的搜索代码:

auto it = umap.find(key);
if (it != umap.end()) 

    klist.erase(it ->second);
    umap.erase(key);


边箱:

由于缺少输入和输出工作,目前不适用于您的代码,但如果您使用 std::cinstd::cout,您可以禁用 C 和 C++ 流之间的同步,并将 cout 中的 cin 解绑为优化:(它们默认绑定在一起)

// If your using cin/cout or I/O
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

【讨论】:

cin.tiecout.tie 调用如何提高这个完全没有 I/O 的特定代码的性能? @selbie 对! (我很快查了一下代码并假设了一个 cin/cout 的实例)已编辑

以上是关于C++ LRU 缓存 - 需要有关如何提高速度的建议的主要内容,如果未能解决你的问题,请参考以下文章

链表(上):如何实现LRU缓存淘汰算法?

C ++中的LRU缓存[重复]

LRU缓存算法与pylru

缓存淘汰算法-LRU 实现原理

缓存淘汰算法-LRU 实现原理

缓存淘汰算法-LRU 实现原理