简体   繁体   中英

C++ LRU cache - need suggestions on how to improve speed

The task is to implement an O(1) Least Recently Used Cache

Here is the question on leetcode
https://leetcode.com/problems/lru-cache/

Here is my solution, while it is O(1) it is not the fastest implementation
could you give some feedback and maybe ideas on how can I optimize this? Thank you !


#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);
 */

Here's some optimizations that might help:

Take this segment of code from the get function:

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

The above will lookup key in the map twice. Once for the count method to see if it exists. Another to invoke the [] operator to fetch its value. Save a few cycles by doing this:

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

In the put function, you do this:

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

Same thing as get , you can avoid the redundant search through umap . Additionally, there's no reason to invoke umap.erase only to add that same key back into the map a few lines later.

Further, this is also inefficient

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

Similar to above, redundantly looking up key twice in the map. In the first assignment statement, the key is not in the map, so it default constructs a new value pair thing. The second assignment is doing another lookup in the map.

Let's restructure your put function as follows:

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);
    }
}

That's as far as you can probably go by using std::list . The downside of the list type is that there's no way to move an existing node from the middle to the front (or back) without first removing it and then adding it back. That's a couple of unneeded memory allocations to update the list. Possible alternative is that you just use your own double-linked list type and manually fixup the prev/next pointer yourself.

Here is my solution, while it is O(1) it is not the fastest implementation could you give some feedback and maybe ideas on how can I optimize this? Thank you !

Gonna take on selbie's point here:
Every instance of if(umap.count(key)) will search for the key and using umap[key] is the equivalent for the search. You can avoid the double search by assigning an iterator which points to the key by a single std::unordered_map::find() operation.

selbie already gave the code for int get() 's search, here's the one for void put() 's one:

auto it = umap.find(key);
if (it != umap.end()) 
{
    klist.erase(it ->second);
    umap.erase(key);
}

Sidecase:

Not applicable for your code as of now due to lack of input and output work, but in case you use std::cin and std::cout , you can disable the synchronization between C and C++ streams, and untie cin from cout as an optimization: (they are tied together by default)

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM