Кэш процессора: уровни L1, L2, L3 и принцип работы

Кэш процессора: уровни L1, L2, L3 и принцип работы Полезное

Кэш процессора — это небольшая, но очень быстрая память внутри чипа, которая хранит копии данных и инструкций из оперативной памяти, чтобы ядро не простаивало в ожидании медленного ОЗУ. Физически это ячейки SRAM, размещенные рядом с вычислительными блоками ядра.

Ниже разберем, зачем нужен кэш, как устроена кэш-строка и что такое попадание и промах, чем различаются уровни L1, L2 и L3, как адрес отображается в кэш и почему порядок доступа к памяти так сильно влияет на производительность. Примеры на Python запущены, вывод в статье — реальный.

Зачем процессору кэш

Ядро процессора работает на частоте несколько гигагерц: за одну наносекунду оно успевает выполнить несколько инструкций. Обращение же к оперативной памяти (DRAM) занимает порядка 60-100 наносекунд. Если бы ядро за каждым числом ходило в ОЗУ, оно сотни тактов простаивало бы впустую.

Кэш закрывает этот разрыв в скорости. Когда ядру нужны данные, оно сначала смотрит в кэш. Если данные там есть — это кэш-попадание (cache hit), ответ приходит за единицы тактов. Если нет — это кэш-промах (cache miss), и данные приходится тянуть из более медленного уровня или из ОЗУ.

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

Кэш-строка и SRAM

Кэш оперирует не отдельными байтами, а блоками фиксированного размера — кэш-строками (cache line). В подавляющем большинстве процессоров x86 и ARM длина строки равна 64 байтам. Даже если ядру нужен один байт, в кэш загружается вся строка целиком.

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

Сама память кэша — это SRAM (Static RAM), а не DRAM, из которой сделано ОЗУ. Ячейка SRAM хранит бит на нескольких транзисторах и не требует регенерации, поэтому работает быстро. Расплата — размер: одна ячейка SRAM занимает на кристалле заметно больше места, чем ячейка DRAM, поэтому кэш измеряется килобайтами и мегабайтами, а не гигабайтами.

Как адрес отображается в кэш

Кэш почти всегда наборно-ассоциативный (set-associative). Адрес данных процессор разбивает на три части: младшие биты — смещение внутри строки (offset), следующие — номер набора (set index), старшие — метку (tag) для сверки. По индексу выбирается набор, в наборе по метке ищется нужная строка.

Посчитаем это разбиение для типового L1 на 32 КБ, 8-way, со строкой 64 байта:

# Как физический адрес раскладывается для кэша L1:
# 32 КБ, 8-way, длина строки 64 байта.
CACHE_SIZE = 32 * 1024      # 32768 байт
LINE = 64                   # длина кэш-строки, байт
WAYS = 8                    # ассоциативность

lines = CACHE_SIZE // LINE          # всего строк
sets = lines // WAYS                # число наборов (sets)
offset_bits = LINE.bit_length() - 1 # биты смещения внутри строки
index_bits = sets.bit_length() - 1  # биты индекса набора

print("строк всего:", lines)
print("наборов (sets):", sets)
print("бит смещения (offset):", offset_bits)
print("бит индекса (index):", index_bits)

for addr in [0x0000, 0x0040, 0x1040, 0x8000]:
    offset = addr & (LINE - 1)
    index = (addr >> offset_bits) & (sets - 1)
    tag = addr >> (offset_bits + index_bits)
    print(f"addr=0x{addr:05x}  tag={tag:<4} set={index:<3} offset={offset}")

Вывод программы:

строк всего: 512
наборов (sets): 64
бит смещения (offset): 6
бит индекса (index): 6
addr=0x00000  tag=0    set=0   offset=0
addr=0x00040  tag=0    set=1   offset=0
addr=0x01040  tag=1    set=1   offset=0
addr=0x08000  tag=8    set=0   offset=0

Смещение занимает 6 бит, потому что в строке 64 байта (2 в степени 6). Адреса 0x0040 и 0x1040 легли в один набор (set 1), но с разными метками — в 8-way наборе они спокойно уживаются рядом. Ассоциативность (число way) — это как раз количество строк, которые могут одновременно жить в одном наборе.

Уровни кэша: L1, L2, L3

Один большой и одновременно быстрый кэш сделать нельзя: чем больше SRAM, тем выше задержка. Поэтому кэш делят на уровни — от маленького и очень быстрого к большому и помедленнее. Цифры ниже — типовые ориентиры для десктопных и серверных процессоров 2026 года, конкретные значения зависят от модели.

Уровень Объем Задержка Кому принадлежит
L1 32-80 КБ на ядро ~4-5 тактов свой у каждого ядра, разделен на L1i и L1d
L2 256 КБ — 4 МБ на ядро ~12-15 тактов обычно свой у ядра (иногда общий на кластер)
L3 от единиц до сотен МБ ~40-70 тактов общий на все ядра (или на кластер ядер)

Уровень L1 стоит развести на две части: L1i хранит инструкции (сам код программы), L1d — данные, с которыми код работает. Раздельные кэши позволяют ядру за один такт и читать следующую инструкцию, и обращаться к данным, не мешая друг другу.

L2 крупнее и чуть медленнее L1; он служит первым запасным уровнем: то, что вытеснено из L1, часто еще лежит в L2. L3 — общий уровень: через него ядра обмениваются данными и находят строки, вытесненные из своих L2.

Именно L3 в серверах и топовых десктопах раздувается сильнее всего. У процессоров AMD с технологией 3D V-Cache L3 доходит до десятков и сотни с лишним мегабайт, а у серверных AMD EPYC последних поколений — до сотен мегабайт суммарно. Точные цифры сильно зависят от модели, их стоит смотреть в спецификации конкретного процессора.

Локальность: почему кэш реально ускоряет

Кэш выгоден не сам по себе, а потому что программы обращаются к памяти предсказуемо. Это называют локальностью: временной (недавно использованные данные скоро понадобятся снова) и пространственной (рядом лежащие данные тоже скоро понадобятся). Загрузка целой строки по 64 байта — ставка именно на пространственную локальность.

Покажем эффект на матрице. Массив int32 хранится по строкам (row-major): элементы одной строки лежат в памяти подряд. Обход по строкам идет вдоль этого порядка, обход по столбцам — поперек. Смоделируем небольшой кэш с вытеснением по LRU и посчитаем промахи:

from collections import OrderedDict

# Модель небольшого кэша с вытеснением по LRU: считаем промахи
# при двух порядках обхода матрицы int32 (4 байта на элемент).
N = 64                      # матрица N x N
ELEM = 4                    # байт на элемент (int32)
LINE = 64                   # длина кэш-строки, байт
PER_LINE = LINE // ELEM     # элементов в одной кэш-строке = 16
CAP = 32                    # вместимость кэша: 32 строки (демо-размер)

def line_of(i, j):
    return (i * N + j) // PER_LINE   # номер кэш-строки для элемента [i][j]

def count_misses(order):
    cache = OrderedDict()   # набор кэш-строк, порядок = давность обращения
    misses = 0
    for i, j in order:
        ln = line_of(i, j)
        if ln in cache:
            cache.move_to_end(ln)        # попадание: строка снова свежая
        else:
            misses += 1                  # промах: грузим строку
            cache[ln] = True
            if len(cache) > CAP:
                cache.popitem(last=False)  # вытесняем самую давнюю (LRU)
    return misses

row_major = [(i, j) for i in range(N) for j in range(N)]   # по строкам
col_major = [(i, j) for j in range(N) for i in range(N)]   # по столбцам

print("элементов в кэш-строке:", PER_LINE)
print("вместимость кэша, строк:", CAP)
print("всего обращений:", N * N)
print("промахов при обходе по строкам:  ", count_misses(row_major))
print("промахов при обходе по столбцам: ", count_misses(col_major))

Вывод программы:

элементов в кэш-строке: 16
вместимость кэша, строк: 32
всего обращений: 4096
промахов при обходе по строкам:   256
промахов при обходе по столбцам:  4096

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

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

Влияние на производительность и как выбирать

Больший кэш — это меньше промахов, но лишь до предела: производители балансируют объем, задержку, площадь кристалла и цену. Наращивать кэш бесконечно бессмысленно: после определенного объема рабочий набор задачи уже помещается, и лишние мегабайты не ускоряют, но удорожают чип.

Ориентир по задаче, а не по каталогу:

  1. Офисный и домашний ПК, веб, учеба. Кэш почти не влияет на ощущения: типовые задачи и так укладываются в L2 и L3. Здесь важнее частота и число ядер, а на объем кэша можно не смотреть.
  2. Рабочая станция: сборка кода, обработка данных, тяжелые IDE, виртуализация. Кэш начинает играть роль: рабочий набор больше, промахи заметны. Осмысленно брать процессоры с крупным L2 и L3.
  3. Игры и вычисления с плотной работой над одним набором данных. Здесь большой L3 (например, версии с 3D V-Cache) дает ощутимый прирост: чаще попадаем в кэш, реже ждем ОЗУ.
  4. Серверы и многоядерные нагрузки. Кэш — одна из ключевых характеристик: при десятках ядер общий L3 в сотни мегабайт снижает конкуренцию за память.

Отдельно про частый миф: увеличить кэш отдельно от процессора нельзя. Кэш физически встроен в кристалл ядра; никакие платы расширения его не наращивают. Единственный способ получить больше кэша — выбрать другой процессор, где он больше по проекту.

Программный кэш — это другое

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

С аппаратным кэшем ЦП его роднит только идея (хранить ближе то, что нужно чаще), но механизм совсем иной. Программный кэш можно очистить руками, например перезагрузив страницу с обновлением (Ctrl + F5) — к кэшу процессора это отношения не имеет, его содержимым управляет только сам чип.

Выводы

  • Кэш процессора — быстрая SRAM-память внутри чипа, буфер между ядром и медленным ОЗУ; хранит копии данных и инструкций, к которым ядро обращается или обратится в ближайшее время.
  • Кэш работает блоками по 64 байта (кэш-строка) и адресуется через набор, метку и смещение; ассоциативность задает, сколько строк живет в одном наборе.
  • Уровни идут от быстрого малого к медленному большому: L1 (свой у ядра, разделен на инструкции и данные), L2 (запасной у ядра), L3 (общий, до сотен МБ на серверах).
  • Выигрыш кэша держится на локальности: последовательный доступ к памяти дает в разы меньше промахов, чем доступ поперек порядка хранения (в модели 256 против 4096).
  • Кэш нельзя нарастить отдельно от процессора; для домашних задач его объем почти не важен, для рабочих станций, игр и серверов — важен.

Где это нужно на практике

Понимание кэша — это база для всего, что касается производительности низкоуровневого кода: структуры данных проектируют с учетом кэш-строк, горячие циклы выстраивают под последовательный доступ к памяти, а не поперек него. Тот же принцип локальности объясняет, почему массив часто быстрее связного списка, хотя алгоритмическая сложность одинакова.

Освойте тему на практике

Если хочется разобраться, как память, кэш и низкоуровневые оптимизации работают на уровне C и системного программирования, посмотрите курс Otus по системному программированию на C. Оценить формат и уровень до старта помогают бесплатные открытые уроки Otus — живые занятия с преподавателями.

FAQ

Что произойдет при полном промахе на всех уровнях? Ядро последовательно проверит L1, L2, L3 и, не найдя данных, обратится в оперативную память; найденная строка по пути заполнит уровни кэша, чтобы следующее обращение было быстрым.

Влияет ли объем кэша на энергопотребление? Да, крупная SRAM занимает площадь кристалла и потребляет ток даже в покое, поэтому в мобильных чипах кэш проектируют компактнее, чем в десктопных и серверных.

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

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)