LRU-алгоритм
LRU-алгоритм (Least Recently Used, «наименее недавно использовавшийся») — это алгоритм вытеснения данных из кэша, который при необходимости освободить место удаляет элемент, к которому дольше всего не было обращений. Относится к классу алгоритмов кэширования и страничной подкачки памяти, основанных на принципе локальности обращений: если данные не использовались долгое время, вероятность их повторного использования в ближайшем будущем ниже, чем у данных, к которым обращались недавно.
Принцип работы
Основная идея LRU заключается в отслеживании времени последнего доступа к каждому элементу кэша. Когда кэш заполнен и требуется вставить новый элемент, алгоритм выбирает элемент с самым старым временем последнего обращения и удаляет его, освобождая место. При каждом обращении к существующему элементу его метка времени обновляется на текущее значение.
Работу LRU можно описать следующей последовательностью шагов:
- При запросе данных сначала проверяется, есть ли они в кэше (cache hit — попадание).
- Если данные есть, они возвращаются, а их метка времени обновляется.
- Если данных нет (cache miss — промах), они загружаются из основного хранилища.
- Если в кэше есть свободное место, данные добавляются с текущей меткой времени.
- Если свободного места нет, находится элемент с наименьшим значением метки времени (самый старый), он удаляется, и на его место помещается новый элемент.
Реализации
LRU может быть реализован различными способами, отличающимися по сложности и производительности.
Простейшая реализация (связанный список + хеш-таблица)
Наиболее распространённая и эффективная реализация использует комбинацию двусвязного списка и хеш-таблицы (словаря). В двусвязном списке элементы хранятся в порядке от самого недавно использованного (в начале списка) до самого давнего (в конце). Хеш-таблица позволяет быстро находить элемент в списке по его ключу.
- Доступ к элементу: по ключу через хеш-таблицу находится узел списка. Если элемент найден, он перемещается в начало списка (удаляется с текущей позиции и вставляется в начало). Время операции — O(1).
- Вставка нового элемента: если кэш не заполнен, новый элемент добавляется в начало списка, а его ключ и ссылка на узел заносятся в хеш-таблицу. Если кэш заполнен, удаляется последний элемент списка (наименее недавно использованный), его запись удаляется из хеш-таблицы, после чего новый элемент добавляется в начало. Время операции — O(1).
- Удаление элемента: элемент удаляется из списка и из хеш-таблицы. Время операции — O(1).
Реализация на основе массива (счётчик)
В системах с ограниченными ресурсами или при небольшом размере кэша может использоваться массив с метками времени. Каждому элементу кэша присваивается счётчик обращений или метка времени. При каждом обращении счётчик текущего элемента сбрасывается, а счётчики остальных элементов увеличиваются. При вытеснении удаляется элемент с наибольшим значением счётчика. Недостаток — необходимость обновлять все счётчики при каждом обращении, что даёт сложность O(n) для операций доступа.
Реализация на основе стека
Стек, в котором при каждом обращении к элементу он перемещается на вершину, а при вытеснении удаляется элемент с дна. Требует O(n) операций для поиска и перемещения элемента в стеке.
Разновидности и модификации
LRU-K
Улучшенная версия, учитывающая не только последнее обращение, а историю последних K обращений. При вытеснении сравнивается время K-го по счёту последнего обращения. Позволяет точнее оценить частоту использования данных, но требует больше памяти для хранения истории.
Segmented LRU (SLRU)
Разделяет кэш на два сегмента: «пробный» (probationary) и «защищённый» (protected). Новые элементы сначала попадают в пробный сегмент. Если к элементу обращаются повторно, он перемещается в защищённый сегмент. Вытеснение происходит из пробного сегмента по LRU, а из защищённого — только при его переполнении, при этом элемент перемещается обратно в пробный. Используется, например, в кэше страниц операционной системы FreeBSD.
Clock (Second Chance)
Приближённая версия LRU, использующая бит доступа (reference bit) вместо точного времени. Элементы организованы в циклический буфер (кольцо). Алгоритм проходит по буферу, проверяя бит доступа: если бит установлен (к элементу обращались), он сбрасывается, и алгоритм переходит к следующему элементу; если бит не установлен, элемент удаляется. Этот алгоритм менее точен, чем LRU, но требует меньше ресурсов и широко применяется в ядрах операционных систем (например, в Linux).
Применение
LRU-алгоритм широко используется в различных областях компьютерных наук и информационных технологий:
- Кэширование страниц памяти в операционных системах (управление виртуальной памятью).
- Кэширование данных в базах данных (например, буферный кэш в PostgreSQL, InnoDB Buffer Pool в MySQL).
- Кэширование веб-контента в прокси-серверах и браузерах (например, кэш DNS, кэш HTTP-ответов).
- Кэширование файлов в файловых системах (например, dentry cache в Linux).
- Кэширование результатов запросов в веб-фреймворках и приложениях.
- Кэширование объектов в библиотеках и фреймворках (например,
lru_cacheв Python из модуляfunctools).
Преимущества и недостатки
Преимущества
- Простота реализации и понимания.
- Хорошая производительность при типичных паттернах доступа, особенно при наличии временной локальности (повторные обращения к недавно использованным данным).
- Гарантированное вытеснение наименее актуальных данных.
Недостатки
- Чувствительность к «сканированию» (scan resistance): если выполняется последовательный обход большого объёма данных, все элементы которого не помещаются в кэш, LRU может вытеснить все ранее кэшированные данные, заместив их одноразово используемыми элементами, что резко снижает эффективность кэширования.
- Потребление памяти: для хранения информации о порядке использования (например, указателей в двусвязном списке) требуется дополнительная память.
- Вычислительные накладные расходы: при каждом обращении необходимо обновлять структуру данных (перемещать элемент в начало списка), что может быть затратным при большом размере кэша и высокой частоте обращений.
- Не учитывает частоту обращений: элемент, к которому обращались часто, но давно, может быть вытеснен раньше, чем элемент, к которому обращались редко, но недавно.
Сравнение с другими алгоритмами
| Алгоритм | Принцип вытеснения | Сложность доступа | Устойчивость к сканированию | Потребление памяти |
|---|---|---|---|---|
| LRU | Наименее недавно использованный | O(1) (с хеш-таблицей) | Низкая | Среднее |
| LFU | Наименее часто используемый | O(log n) или O(1) с приближением | Высокая | Высокое |
| FIFO | Первый вошёл — первый вышел | O(1) | Средняя | Низкое |
| ARC | Адаптивное сочетание LRU и LFU | O(1) | Высокая | Среднее |
| Clock | Приближение LRU с битом доступа | O(1) | Средняя | Низкое |
Интересные факты
- LRU-алгоритм был впервые описан в 1965 году в работе Ласло Белади (László Belády) «A Study of Replacement Algorithms for Virtual Storage Computers».
- В 1970-х годах было доказано, что LRU не является оптимальным алгоритмом вытеснения для всех паттернов доступа. Оптимальный алгоритм (Belady's algorithm) требует знания будущих обращений, что невозможно на практике.
- В современных процессорах для кэширования данных используется не LRU, а его приближённые версии (например, псевдо-LRU), поскольку реализация точного LRU в аппаратуре требует слишком много транзисторов и энергии.
- В Python декоратор
@functools.lru_cacheпозволяет легко добавить кэширование с LRU-вытеснением к любой функции, что широко используется для оптимизации рекурсивных и вычислительно сложных функций.
Источники
- Belady, L. A. (1966). A Study of Replacement Algorithms for Virtual Storage Computers. IBM Systems Journal, 5(2), 78–101.
- Tanenbaum, A. S., & Bos, H. (2015). Modern Operating Systems (4th ed.). Pearson.
- Silberschatz, A., Galvin, P. B., & Gagne, G. (2018). Operating System Concepts (10th ed.). Wiley.
- Corbato, F. J. (1968). A Paging Experiment with the Multics System. MIT Project MAC Report.
- Документация Python:
functools.lru_cache— Python 3.12.2 documentation.
BFOmetr — база данных и аналитика по компаниям России.
На главную BFOmetr →