Кэш процессора — это небольшая, но очень быстрая память внутри чипа, которая хранит копии данных и инструкций из оперативной памяти, чтобы ядро не простаивало в ожидании медленного ОЗУ. Физически это ячейки 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, без разделения на инструкции и данные), но она верно передает главное: один и тот же алгоритм с одними и теми же данными на реальном железе может отличаться по скорости в разы только из-за порядка доступа к памяти.
Влияние на производительность и как выбирать
Больший кэш — это меньше промахов, но лишь до предела: производители балансируют объем, задержку, площадь кристалла и цену. Наращивать кэш бесконечно бессмысленно: после определенного объема рабочий набор задачи уже помещается, и лишние мегабайты не ускоряют, но удорожают чип.
Ориентир по задаче, а не по каталогу:
- Офисный и домашний ПК, веб, учеба. Кэш почти не влияет на ощущения: типовые задачи и так укладываются в L2 и L3. Здесь важнее частота и число ядер, а на объем кэша можно не смотреть.
- Рабочая станция: сборка кода, обработка данных, тяжелые IDE, виртуализация. Кэш начинает играть роль: рабочий набор больше, промахи заметны. Осмысленно брать процессоры с крупным L2 и L3.
- Игры и вычисления с плотной работой над одним набором данных. Здесь большой L3 (например, версии с 3D V-Cache) дает ощутимый прирост: чаще попадаем в кэш, реже ждем ОЗУ.
- Серверы и многоядерные нагрузки. Кэш — одна из ключевых характеристик: при десятках ядер общий L3 в сотни мегабайт снижает конкуренцию за память.
Отдельно про частый миф: увеличить кэш отдельно от процессора нельзя. Кэш физически встроен в кристалл ядра; никакие платы расширения его не наращивают. Единственный способ получить больше кэша — выбрать другой процессор, где он больше по проекту.
Программный кэш — это другое
Слово «кэш» встречается и на уровне программ: кэш браузера, кэш приложения, кэш веб-сервера. Это тоже буфер для часто используемых данных, но живет он в обычной оперативной памяти или на диске, а управляет им программа, а не процессор.
С аппаратным кэшем ЦП его роднит только идея (хранить ближе то, что нужно чаще), но механизм совсем иной. Программный кэш можно очистить руками, например перезагрузив страницу с обновлением (Ctrl + F5) — к кэшу процессора это отношения не имеет, его содержимым управляет только сам чип.
Выводы
- Кэш процессора — быстрая SRAM-память внутри чипа, буфер между ядром и медленным ОЗУ; хранит копии данных и инструкций, к которым ядро обращается или обратится в ближайшее время.
- Кэш работает блоками по 64 байта (кэш-строка) и адресуется через набор, метку и смещение; ассоциативность задает, сколько строк живет в одном наборе.
- Уровни идут от быстрого малого к медленному большому: L1 (свой у ядра, разделен на инструкции и данные), L2 (запасной у ядра), L3 (общий, до сотен МБ на серверах).
- Выигрыш кэша держится на локальности: последовательный доступ к памяти дает в разы меньше промахов, чем доступ поперек порядка хранения (в модели 256 против 4096).
- Кэш нельзя нарастить отдельно от процессора; для домашних задач его объем почти не важен, для рабочих станций, игр и серверов — важен.
Где это нужно на практике
Понимание кэша — это база для всего, что касается производительности низкоуровневого кода: структуры данных проектируют с учетом кэш-строк, горячие циклы выстраивают под последовательный доступ к памяти, а не поперек него. Тот же принцип локальности объясняет, почему массив часто быстрее связного списка, хотя алгоритмическая сложность одинакова.
Освойте тему на практике
Если хочется разобраться, как память, кэш и низкоуровневые оптимизации работают на уровне C и системного программирования, посмотрите курс Otus по системному программированию на C. Оценить формат и уровень до старта помогают бесплатные открытые уроки Otus — живые занятия с преподавателями.
FAQ
Что произойдет при полном промахе на всех уровнях? Ядро последовательно проверит L1, L2, L3 и, не найдя данных, обратится в оперативную память; найденная строка по пути заполнит уровни кэша, чтобы следующее обращение было быстрым.
Влияет ли объем кэша на энергопотребление? Да, крупная SRAM занимает площадь кристалла и потребляет ток даже в покое, поэтому в мобильных чипах кэш проектируют компактнее, чем в десктопных и серверных.
Что такое ложное разделение (false sharing)? Это ситуация, когда два ядра пишут в разные переменные, случайно попавшие в одну кэш-строку: строка постоянно гоняется между ядрами, и производительность падает, хотя логически конфликта данных нет.



