# теория · шаг 2 из 10
Кеш страниц: LRU, FIFO и оптимум Белади
Коротко
В кеш помещается k страниц; при промахе какую-то надо выселить, не зная будущих запросов. Оптимум,
знающий будущее, выселяет ту, что понадобится позже всех (алгоритм Белади, MIN). Онлайн-правила
LRU (выселить давно не использованную) и FIFO (выселить самую старую по приходу) -
k-конкурентны, и лучше k не может ни одно детерминированное правило: противник каждый раз просит
страницу, которой нет в кеше, и любое такое правило промахивается на каждом запросе, а оптимум - раз в
k. Для LRU и FIFO таким противником оказывается простой круг из k + 1 страниц. LFU
(выселить самую редко используемую) не конкурентен вовсе: страница, популярная в прошлом, занимает
место, пока счётчики новых страниц её не догонят.
Если ты знаешь, почему алгоритм Белади оптимален, почему LRU и FIFO k-конкурентны, почему лучше k не может ни одно детерминированное правило и почему LFU не конкурентен, листай до «Теперь сам».
Как думаешь, кого выселять
Кеш на три страницы, запросы 1, 2, 3, 1, 4, …. На запросе 4 кеш полон: 1, 2, 3. Кого выселить? Двойку,
которую давно не трогали? Тройку, которая пришла последней? Единицу, к которой обращались чаще всех?
Ответь себе до чтения: какое правило лучше на худшем входе? Интуиция подсказывает «самую редко используемую» - частота кажется самым надёжным признаком. Это правило - единственное из трёх, у которого худший случай вообще не ограничен. Оно помнит прошлое слишком хорошо.
На что это похоже
Рабочий стол, на котором помещаются три папки. Остальные в шкафу, доставать долго. Если бы ты знал расписание совещаний на неделю, ты бы убирал в шкаф ту папку, которая понадобится позже всех, - это и есть алгоритм Белади. Расписания нет, и ты убираешь ту, которую давно не открывал (LRU), или ту, что лежит дольше всех (FIFO).
LFU - это «убирать папку, которую реже всех открывал за всю жизнь». Папка прошлогоднего отчёта, открытая сто раз в декабре, будет лежать на столе вечно, а две текущие папки будут меняться местами в шкафу при каждом обращении.
Механизм: оптимум, фазы и противник
Алгоритм Белади (1966). При промахе выселить страницу, следующий запрос к которой дальше всех (или
которой больше не будет). Он оптимален: если какой-то план выселяет страницу p раньше q, хотя q
нужна позже, их можно поменять ролями, и промахов не станет больше. Это доказательство обменом, как у
жадных алгоритмов.
LRU k-конкурентен. Разрежем последовательность запросов на фазы: каждая фаза - наибольший
отрезок, в котором встречается не больше k разных страниц.
в фазе k разных страниц → LRU промахивается в фазе
не больше k раз: загруженная в фазе
страница до конца фазы не выселяется
k страниц фазы + первая страница следующей = k + 1 разных
→ в кеше на k мест любой план промахивается хотя бы раз
на каждом таком отрезке
итого: LRU <= k · (число фаз) <= k · OPT + k
Для FIFO довод тот же. Сравнение с оптимумом идёт не по запросам, а по фазам: в фазе LRU ошибается до
k раз, а оптимум - хотя бы раз на каждом отрезке от второго запроса фазы до первого запроса
следующей.
Нижняя граница k. Противник держит k + 1 страниц и каждый раз просит ту, которой нет в кеше
алгоритма. Детерминированный алгоритм промахивается на каждом запросе. Оптимум на тех же запросах,
выселяя страницу, нужную позже всех, после заполнения кеша промахивается не чаще раза в k
запросов. Добавка в пару промахов на заполнение - обычная часть определения: ALG ≤ c · OPT + b.
LFU. Страница 1 запрошена m раз подряд, потом чередуются 2 и 3. При кеше на две страницы LFU
держит 1 (частота m) и выселяет 2 ради 3 и 3 ради 2 - промах на каждом запросе. Оптимум
выселяет 1 сразу. Отношение растёт вместе с m без ограничения.
Правило
Оптимум при известном будущем - выселять страницу, нужную позже всех (Белади). LRU и FIFO
k-конкурентны: в каждой фазе из k разных страниц они промахиваются не больше k раз, а оптимум -
хотя бы раз. Ни одно детерминированное правило не лучше k. LFU не конкурентен: старая популярность
занимает место навсегда.
Разбор: одна трасса, четыре правила
promahi моделирует кеш на k страниц: при промахе выселяется страница с наименьшим ключом, а ключ
зависит от правила. Для LRU - время последнего обращения, для FIFO - время прихода, для LFU - число
обращений, для MIN - минус время следующего обращения (выселяется нужная позже всех).
def sled(zaprosy, t, x):
for u in range(t + 1, len(zaprosy)):
if zaprosy[u] == x:
return u
return len(zaprosy)
def promahi(zaprosy, k, pravilo):
kesh = []
poslednij = {}
prishyol = {}
chastota = {}
mimo = 0
for t in range(len(zaprosy)):
s = zaprosy[t]
chastota[s] = chastota.get(s, 0) + 1
if s not in kesh:
mimo = mimo + 1
if len(kesh) == k:
# выселяется страница с наименьшим ключом
klyuchi = {}
for x in kesh:
if pravilo == "LRU":
klyuchi[x] = poslednij[x]
elif pravilo == "FIFO":
klyuchi[x] = prishyol[x]
elif pravilo == "LFU":
klyuchi[x] = chastota[x]
else:
klyuchi[x] = -sled(zaprosy, t, x)
zhertva = kesh[0]
for x in kesh:
if klyuchi[x] < klyuchi[zhertva]:
zhertva = x
kesh.remove(zhertva)
kesh.append(s)
prishyol[s] = t
poslednij[s] = t
return mimo
trassa = [1, 2, 3, 1, 4, 1, 2, 5, 1, 2, 3, 4, 5, 1, 2, 1, 3, 1, 4, 2]
for pravilo in ["LRU", "FIFO", "LFU", "MIN"]:
print(pravilo, promahi(trassa, 3, pravilo))
LRU 14
FIFO 13
LFU 11
MIN 10
Двадцать запросов, кеш на три страницы. Оптимум - 10 промахов. LFU на этой трассе лучший из онлайн-правил (11): страница 1 действительно популярна и действительно нужна. LRU - худший (14). Если бы выбор правила решался одной трассой, победил бы LFU - и это была бы ошибка, которую показывает следующий разбор.
Разбор: противник для каждого правила
Те же функции. Сначала круг из k + 1 страниц, восемь оборотов - противник для LRU и FIFO. Потом
противник для LFU: страница 1 запрошена m раз, потом m раз пара 2, 3; кеш на две страницы.
def sled(zaprosy, t, x):
for u in range(t + 1, len(zaprosy)):
if zaprosy[u] == x:
return u
return len(zaprosy)
def promahi(zaprosy, k, pravilo):
kesh = []
poslednij = {}
prishyol = {}
chastota = {}
mimo = 0
for t in range(len(zaprosy)):
s = zaprosy[t]
chastota[s] = chastota.get(s, 0) + 1
if s not in kesh:
mimo = mimo + 1
if len(kesh) == k:
# выселяется страница с наименьшим ключом
klyuchi = {}
for x in kesh:
if pravilo == "LRU":
klyuchi[x] = poslednij[x]
elif pravilo == "FIFO":
klyuchi[x] = prishyol[x]
elif pravilo == "LFU":
klyuchi[x] = chastota[x]
else:
klyuchi[x] = -sled(zaprosy, t, x)
zhertva = kesh[0]
for x in kesh:
if klyuchi[x] < klyuchi[zhertva]:
zhertva = x
kesh.remove(zhertva)
kesh.append(s)
prishyol[s] = t
poslednij[s] = t
return mimo
for k in [3, 5]:
po_krugu = []
for povtor in range(8):
for stranica in range(1, k + 2):
po_krugu.append(stranica)
print(k, len(po_krugu), promahi(po_krugu, k, "LRU"),
promahi(po_krugu, k, "FIFO"), promahi(po_krugu, k, "MIN"))
for m in [5, 20]:
zaprosy = [1] * m
for povtor in range(m):
zaprosy = zaprosy + [2, 3]
print(m, promahi(zaprosy, 2, "LFU"), promahi(zaprosy, 2, "LRU"),
promahi(zaprosy, 2, "MIN"))
3 32 32 32 13
5 48 48 48 14
5 11 3 3
20 41 3 3
Круг из четырёх страниц при кеше на три: 32 запроса, LRU и FIFO промахиваются все 32 раза, оптимум - 13.
Круг из шести страниц при кеше на пять: 48 против 14. Отношение
растёт вместе с k, как и обещает нижняя граница: на круге оптимум промахивается примерно раз в k
запросов.
Противник для LFU: при m = 5 - 11 промахов против 3 у LRU и оптимума, при m = 20 - 41 против 3. LFU
выиграл на первой трассе и проигрывает здесь во сколько угодно раз. LRU на тех же запросах сразу
отпустил страницу 1, как и оптимум.
Что ломается без этого
Выбирают политику кеша по одной трассе. LFU на обычной трассе выглядит лучше LRU, а на смене режима работы - катастрофа: вчерашние горячие данные не уходят.
Удивляются, что LRU «в k раз хуже оптимума». Это худший случай на специальной последовательности. На
реальных запросах с локальностью LRU близок к оптимуму, а если дать ему чуть больше памяти, чем
оптимуму, гарантия резко улучшается: при k местах против h у оптимума - k/(k − h + 1) (Слейтор,
Тарьян, 1985). Вдвое больший кеш - отношение не больше двух.
Обходят большой файл через кеш. Последовательный проход по файлу больше кеша - это круг противника: каждый запрос - промах, и полезные страницы вымыты. Поэтому базы данных и ОС защищают кеш от таких проходов, держа новые страницы на «испытательном сроке».
Зачем это в работе
Кеш страниц ОС, буферный пул базы данных, CDN, кеш в памяти приложения - везде есть выбор политики
выселения. LRU и его приближения (часы, LRU-K, 2Q, ARC) - стандарт ровно потому, что у LRU доказанный
худший случай, а у LFU его нет. Знание нижней границы k объясняет, почему циклический проход больше
кеша убивает производительность при любой из обычных политик, и подсказывает лечение: не политика, а больше
памяти или другой порядок обхода.
Теперь сам
- Кеш на две страницы, запросы
1, 2, 3, 1, 2, 3. Сколько промахов у LRU и у MIN? - Почему алгоритм Белади нельзя использовать в настоящем кеше?
- Кеш на 100 страниц. Какое наибольшее отношение к оптимуму может получить LRU и на каком входе?
Ответ. 1. LRU: каждый запрос - промах, 6. MIN: промахи на 1, 2, 3 (выселяем 2, нужную позже 1), 1 - попадание, 2 - промах (выселяем 1, она больше не нужна), 3 - попадание. Итого 4.
-
Он смотрит в будущее: какой запрос к странице будет следующим. Онлайн-кеш этого не знает; MIN служит меркой, с которой сравнивают настоящие правила.
-
Около 100 - на круге из 101 страницы, где каждый запрос - к странице, только что выселенной.
Главное
Оптимум выселяет то, что понадобится позже всех, а онлайн-правила угадывают по прошлому. LRU угадывает
так, что проигрывает не больше чем в k раз - и лучше без знания будущего нельзя; LFU цепляется за
прошлое и может проиграть во сколько угодно раз.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий