Кэш C++ LRU - нужны предложения по повышению скорости

Задача состоит в том, чтобы реализовать 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);
 */


person auxeon    schedule 27.04.2020    source источник
comment
размер карты технически никогда не должен пересекать емкость, иначе это ошибочный дизайн   -  person auxeon    schedule 28.04.2020
comment
Имеет смысл с точки зрения реальной ОС.   -  person nice_dev    schedule 28.04.2020


Ответы (2)


Вот некоторые оптимизации, которые могут помочь:

Возьмите этот сегмент кода из функции 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.

person selbie    schedule 27.04.2020
comment
Вы можете использовать std::list::splice для перемещения узла в том же списке, включая начало или конец. - person Blastfurnace; 27.04.2020
comment
@selbie Спасибо !! Я попробовал ваши предложения, хотя код улучшился, теперь он работает за 172 мс вместо 184 мс, а также чем umap.emplace(key,itempair) отличается от umap[key] = itempair. как я понял unordered_map вставки и поиски - это O(1) . - person auxeon; 27.04.2020
comment
@Blastfurnace пытается использовать klist.splice(klist.end(),klist,it-›second.second); дает мне [ AddressSanitizer : alloc-dealloc-mismatch. ] Это правильный способ переместить промежуточный узел в конец списка? - person auxeon; 27.04.2020
comment
@auxeon Это выглядит правильно. Я не в том месте, где я могу протестировать что-либо, кроме это код, который я использовал и принято на LeetCode. Обратите внимание, что я сращиваю спереди, но сращивание сзади тоже должно работать. - person Blastfurnace; 27.04.2020

Вот мое решение, хотя это O (1), это не самая быстрая реализация, не могли бы вы дать отзыв и, возможно, идеи о том, как я могу это оптимизировать? Спасибо !

Возьмем точку зрения Селби здесь:
Каждый экземпляр 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::cin и std::cout, вы можете отключить синхронизацию между потоками C и C++ и отвязать cin от cout в качестве оптимизации: (они привязаны вместе по умолчанию)

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

person Anirban166    schedule 27.04.2020
comment
Как эти вызовы cin.tie и cout.tie улучшают производительность для этого конкретного кода, который вообще не выполняет ввод-вывод? - person selbie; 27.04.2020
comment
@selbie Верно! (Я быстро просмотрел код и предположил, что это экземпляр cin/cout) Отредактировано - person Anirban166; 27.04.2020