Чому кеші потребують політики виключення
Кеш існує для підтримки невеликого, швидкого набору елементів поруч, щоб повторні запити не доводилося отримувати за додатковою вартістю з повільнішого джерела, будь то основна пам'ять, диск, база даних або віддалений сервер. Але швидке зберігання завжди рідкісне: кеш CPU вимірюється в мегабайтах, пул буферів бази даних – у гігабайтах, а вузол CDN має частку простору порівняно з початковим сервером, який він захищає. Коли ця фіксована ємність заповнена, додавання нового елемента означає, що вже всередині доведеться залишити. Правило, яке визначає, який елемент залишається, це політика виключення, і вона має величезне значення для продуктивності. Погана політика видаляє дані, які ось-ось будуть використані знову, змушуючи дорогий повторний отримання та перетворюючи кеш на надмірність, а не прискорення. Добра політика підтримує елементи, найімовірніше, будуть використовуватися, і очищає ті, які давно не використовувалися. Оскільки реальні робочі навантаження схильні демонструвати часову локальність – якщо щось було доступно нещодавно, то ймовірність того, що воно буде доступне знову незабаром, непропорційно висока – політики виключення, які відстежують несвідомість, зазвичай дуже добре працюють на практиці. Це вся мотивація для підходу «Найменш Нещодавно Використаний»: замість того, щоб вгадувати випадково або виключати відповідно до порядку прибуття, він використовує власний історію доступу кожного елемента як сигнал щодо того, чи потрібно його зберігати.
Принцип LRU
Найменша нещодавно використана евакуація слідує за одним правилом: коли кеш повний і потрібно додати новий елемент, видаляється той елемент, який не використовувався найдовше. Кожного разу, коли елемент читається або записується, він розглядається як нещодавно використаний і переміщується на початок уявного чергу за частотою використання. Елемент, що знаходиться в кінці цього чергу, тобто той, який не торкався найдовше, є завжди кандидатом на евакуацію. Це робить LRU гарним наближенням ідеальної (але практично неможливою) стратегії виведення того, що буде потрібно найдалі в майбутньому, оскільки нещодавнє використання часто є найкращим доступним передбаченням для близького майбутнього. Два операції визначають політику: get, який отримує елемент, якщо він присутній, і позначає його як нещодавно використаний, та put, який вставляє або оновлює елемент і, якщо кеш вже наповнений, запускає евакуацію найменше нещодавно використаного запису першим. Елегантність LRU полягає в тому, що їй не потрібні жодні статистичні дані, жодна лічильник, жоден прогнозний модель; їй потрібно лише запам'ятати порядок. Ця простота дозволяє їй ефективно реалізуватися, що є предметом наступного розділу.
Хеш-мапа плюс Двосторонняя связная очередь
Класична реалізація LRU поєднує дві структури даних так, щоб кожне операцію виконувалося за постійного часу, незалежно від того, скільки елементів містить кеш. Хеш-мапа зберігає ключ, зіставлений з розташуванням вузла, що забезпечує постійний час пошуку: за заданим ключем ви миттєво знаходите відповідне запис кеша без сканування. Двостороння побіжна черга переплітає всі записи в порядку нещодавнього використання, з найнещодавнішим елементом у хвості та найрідше використаним — у шапочці. Оскільки це двостороння побіжна черга, будь-який вузол можна відключити від його поточного положення та перев’язати в іншому місці за постійного часу, без обходу списку. При отриманні хеш-мапа миттєво знаходить вузол, і вузол відключається та повторно вставляється на початок, оскільки тепер це найнещодавніше використаний елемент — обидва це операції з покажчиками за постійного часу. При додаванні для нового ключа створюється вузол, додається на початок і реєструється у хеш-мапі; якщо кеш був повним, вузол на кінці відключається та його ключ видаляється з карти, що є точною етапом виселення. При додаванні для оновленого ключа вузол переміщується на початок як при отриманні. Окремо жодна з цих структур не працювала б так добре: хеш-мапа сама по собі не має сенсу порядку, а проста масивна черга потребує лінійного часу для переміщення або видалення записів. Разом вони забезпечують O(1) пошук і O(1) переупорядкування, комбінація, яка робить LRU практичною навіть у великих масштабах.
Розрахунковий Приклад, Крок за Кроком
Розглянемо кеш із місткістю 3 та послідовність доступу до ключів A, B, C, D, B, E. Почніть з пустого кешу. Доступ до A: відсутній, вставте його; порядок від найновішого до найстарішого – A. Доступ до B: відсутній, вставте його; порядок – B, A. Доступ до C: відсутній, вставте його; порядок – C, B, A та кеш заповнений. Доступ до D: відсутній і кеш заповнений, тому виженіть найменш нещодавній елемент, який є A на кінці; вставте D на початок; порядок – D, C, B, а A тепер зник. Доступ до B: він присутній, тому це вважається успіхом; перемістіть B на початок без вигнання будь-чого; порядок – B, D, C. Доступ до E: відсутній і кеш заповнений, тому виженіть поточний кінець, який є C; вставте E на початок; порядок – E, B, D, а C тепер зник. Зверніть увагу, що B вижив першого етапу вигнання саме завдяки тому, що його було повторно викликано до того, як воно повністю застаріло на кінці, тоді як A та C кожного разу виганялися, коли вони ставали найменш нещодавнім записом у повному кеші. Цей трас показує механіку, яку робить видимою симулятор в реальному часі: кожен доступ або просуває елемент на передній план, або викликає саме один вигнання з кінця, а комбінація хеш-мапи та зв’язного списку виконує кожен із цих кроків без жодного сканування всього кешу.
Порівняння політик та практичне застосування
LRU є однією з декількох стратегій виведення (eviction strategies), і правильний вибір залежить від шаблону доступу. FIFO (першим у – першим виходить) виводить елемент, який було введено найраніше, не звертаючи уваги на те, як нещодавно він використовувався; це простіше реалізувати, але працює гірше, коли старий елемент часто використовується, оскільки FIFO виводить його, коли настав його час, незалежно від того, чи він ще актуальний. LFU (найменш використовуваний) відстежує, скільки разів кожен елемент був використаний, і виводить той, що має найнижчий показник використання; це заохочує елементи, які є популярними в довгостроковій перспективі, але може мати труднощі з адаптацією, коли раніше популярний елемент раптово стає неактуальним, оскільки його високий історичний рахунок тримає його в кеші довше, ніж він повинен. LRU знаходиться між цими двома: він швидко реагує на змінні шаблони, оскільки піклується лише про нещодавність використання, а не про накопичену історію, що робить його сильним загальним призначенням за замовчуванням. Ці компроміси мають значення не тільки в підручниках, але й у реальному світі. CPU кеші використовують політики, подібні до LRU (часто наближені для швидкості в апаратному забезпеченні), щоб вирішити, які рядки кешу тримати, коли програми отримують доступ до пам’яті. Буферні пули баз даних використовують LRU або варіанти LRU для визначення, які сторінки диска залишаються в пам'яті, оскільки повторний читання сторінки з диска набагато повільніше, ніж обслуговування її з RAM. Кеші країв веб- та CDN використовують LRU для визначення, які кешовані відповіді тримати на вузлах країв, близьких до користувачів, виводячи застарілий або рідко запитуваний контент, щоб звільнити місце для того, що зараз популярне. У кожній з цих систем одна й та сама схема хеш-мапи плюс подвійні зв’язані списки, або її близький варіант, виконує роботу за лаштунками.
Часті запитання
Why is LRU implemented with both a hash map and a doubly linked list instead of just one structure?
Hash map alone забезпечує швидкий доступ за ключем, але не враховує послідовність, тому не може визначити найрідше використаний елемент без додаткового обліку. Doubly linked list забезпечує порядок і постійне переупорядкування вузла, але пошук вузла за ключем вимагає сканування всього списку. Об'єднання їх означає, що hash map миттєво знаходить вузол, а linked list миттєво його видаляє або переставляє, таким чином, кожна операція виконується за постійного часу.
What is the time complexity of get and put operations in an LRU cache?
Обидва операції виконуються за постійного часу, в термінах O 1, оскільки hash map забезпечує прямий доступ до розташування вузла, а linked list дозволяє безперервно видаляти та переставляти цей вузол без проходження інших вузлів.
How does LRU differ from LFU?
LRU вимикає елементи на основі нещодавньої використання, видаляючи той елемент, який не використовувався найдовше. LFU вимикає елементи на основі частоти, видаляючи той елемент з найменшою кількістю доступу. LRU швидко адаптується до змін у шаблонах доступу, тоді як LFU винагороджує довгострокову популярність, але може бути повільним у відмові від елементів, які раніше були популярними, але більше не потрібні.
How does LRU differ from FIFO?
FIFO вимикає строго за порядком вставлення, незалежно від того, як часто або нещодавно використовувався елемент, тому часто використовуваний елемент все ще може бути видалений просто тому, що він був вставлений раніше. LRU замінює позицію елемента кожного разу, коли він використовується, таким чином, часто використовувані або нещодавно використані елементи захищені від вимикання навіть якщо вони були вставлені давно.
Where is LRU eviction actually used in real systems?
LRU та LRU-спрямовані політики використовуються в апаратному кеші ЦП для визначення, які блоки кешу потрібно зберігати, у буферних пулах баз даних для визначення, які сторінки диска залишаються в пам'яті та в веб-кешах і CDN edge nodes для визначення, який вміст кешу потрібно тримати близько до користувачів, коли сховище на краю обмежене.
Спробуйте наживо
Усе, що вище, працює прямо у вашому браузері — відкрийте LRU Cache: Evicting the Least Recently Used Item і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.
▶ Відкрити симуляцію LRU Cache: Evicting the Least Recently Used Item