Наименее недавно использованный кеш с использованием C ++ - PullRequest
10 голосов
/ 04 сентября 2010

Я пытаюсь реализовать LRU Cache с использованием C ++. Я хотел бы знать, что является лучшим дизайном для их реализации. Я знаю, что LRU должен предоставить find (), добавить элемент и удалить элемент. Удаление должно удалить элемент LRU. Каковы лучшие ADT для реализации этого Например: если я использую карту с элементом в качестве значения и счетчик времени в качестве ключа, я могу искать за время O (logn), вставка - O (n), удаление - O (logn).

Ответы [ 7 ]

14 голосов
/ 04 сентября 2010

Одна из основных проблем с LRU-кешами состоит в том, что существует мало «const» операций, большинство из них изменят базовое представление (хотя бы потому, что они увеличивают доступ к элементу).

Это, конечно, очень неудобно, потому чтоэто означает, что это не традиционный контейнер STL, и поэтому любая идея выставления итераторов довольно сложна: когда разыменовывается итератор, это доступ, который должен изменить список, по которому мы итерируем ... ой мой.

И есть соображения производительности, как с точки зрения скорости, так и с точки зрения потребления памяти.

К сожалению, вам понадобится какой-то способ организовать ваши данные в очередь (LRU) (с возможностью удаленияэлементы из середины), и это означает, что ваши элементы должны быть независимы друг от друга.A std::list подходит, конечно, но это больше, чем нужно.Здесь достаточно односвязного списка, так как вам не нужно повторять список в обратном направлении (в конце концов, вы просто хотите очередь).

Однако одним из основных недостатков является их плохое месторасположение,если вам нужна большая скорость, вам нужно будет предоставить свой собственный распределитель (пул?) для узлов, чтобы они были как можно ближе друг к другу.Это также несколько облегчит фрагментацию кучи.

Далее вам, очевидно, потребуется структура индекса (для бита кеша).Наиболее естественным является обращение к хэш-карте.std::tr1::unordered_map, std::unordered_map или boost::unordered_map обычно являются качественной реализацией, некоторые из них должны быть доступны для вас.Они также выделяют дополнительные узлы для обработки коллизий хешей, вы можете предпочесть другие виды хеш-карт, посмотрите статью Википедии на эту тему и прочитайте о характеристиках различных методов реализации.

Продолжениеесть (очевидная) поддержка потоков.Если вам не нужна поддержка потоков, тогда все в порядке, если вы делаете, однако, это немного сложнее:

  • Как я уже сказал, на такой структуре мало операций const, поэтомувам на самом деле не нужно различать доступ для чтения / записи
  • Внутренняя блокировка в порядке, но вы можете обнаружить, что она не подходит для ваших целей.Проблема с внутренней блокировкой заключается в том, что она не поддерживает концепцию «транзакции», поскольку освобождает блокировку между каждым вызовом.Если это ваш случай, преобразуйте ваш объект в мьютекс и предоставьте метод std::unique_ptr<Lock> lock() (в отладке вы можете утверждать, что блокировка берется в точке входа каждого метода)
  • Есть (в блокировкестратегии) ​​проблема повторного входа, т. е. возможность "заблокировать" мьютекс из одного потока, проверьте Boost.Thread для получения дополнительной информации о различных доступных блокировках и мьютексах

Наконец, естьпроблема сообщения об ошибках.Поскольку ожидается, что кэш не сможет извлечь введенные вами данные, я бы подумал об использовании исключения «плохой вкус».Рассмотрим либо указатели (Value*), либо Boost. Опционально (boost::optional<Value&>).Я бы предпочел Boost.Optional, потому что его семантика ясна.

7 голосов
/ 04 сентября 2010

Лучший способ реализовать LRU - использовать комбинацию std :: list и stdext :: hash_map (хотите использовать только std, а затем std :: map).

  • Сохраните данные в списке так, чтобы наименее недавно использовался в на последний и используйте карту, чтобы указать на элементы списка.
  • Для «получения» используйте карту, чтобы получить список addr и получить данные и переместить текущий узел в
    сначала (так как это использовалось сейчас) и обновите карту.
  • Для "вставки" удалите последний элемент из списка и добавьте новые данные вперед и обновите карту.

Это самое быстрое, что вы можете получить. Если вы используете hash_map, вы должны почти все операции выполнить в O (1). Если используется std :: map, он должен принимать O (logn) во всех случаях.

Очень хорошая реализация доступна здесь

5 голосов
/ 15 декабря 2010

Эта статья описывает пару реализаций C ++ LRU-кэша (одна с использованием STL, другая с boost::bimap).

2 голосов
/ 04 сентября 2010

Я бы вообще не делал кэш видимым для внешнего мира, если бы мог его избежать. Я бы просто имел коллекцию (чего угодно) и обрабатывал бы кэширование незаметно, добавляя и удаляя элементы по мере необходимости, но внешний интерфейс был бы точно таким же, как и у базовой коллекции.

Что касается реализации, куча, пожалуй, самая очевидная. Он имеет сложности, примерно похожие на карту, но вместо построения дерева из связанных узлов он упорядочивает элементы в массиве, и «ссылки» неявно основаны на индексах массива. Это увеличивает плотность хранения вашего кеша и улучшает локальность в «реальном» (физическом) кеше процессора.

2 голосов
/ 04 сентября 2010

Когда вы говорите «приоритет», я думаю « heap », что, естественно, приводит к клавише увеличения и delete-min .

0 голосов
/ 04 сентября 2010

Я бы пошел с нормальной кучей в C ++.

При использовании std :: make_heap (согласно стандарту гарантируется, что O (n)), std :: pop_heap и std :: push_heap в #include, его реализация была бы абсолютно легкой. Вам нужно только беспокоиться о клавише увеличения.

0 голосов
/ 04 сентября 2010

Я предлагаю кучу и, возможно, Куча Фибоначчи

...