Кэш процессора существует по одной причине: разрыв в скорости между исполнительными устройствами и оперативной памятью. Ядро способно выполнять несколько операций за такт, а обращение в память занимает сотни тактов ожидания. Без промежуточных буферов ядро простаивало бы большую часть времени. Кэш — это не «ускоритель» в общем смысле, а компенсатор конкретной задержки, и понимание этого сразу объясняет, почему прирост от большего объёма в одних задачах виден отчётливо, а в других отсутствует.
Иерархия уровней и её логика
Кэш строится многоуровневым, потому что быстрая память дорога по площади кристалла и по энергии. Первый уровень небольшой, разделён на часть для инструкций и часть для данных, привязан к конкретному ядру и отвечает за считанные такты. Второй уровень больше, тоже принадлежит ядру, отвечает медленнее. Третий уровень крупный и общий для группы ядер, задержка у него уже десятки тактов, но всё равно на порядок меньше, чем у обращения в оперативную память.
Данные подтягиваются не по одному значению, а строками фиксированной длины. Это принципиально: если программа читает массив последовательно, одна загрузка строки обслуживает сразу несколько следующих обращений. Если же она прыгает по памяти случайным образом, каждая строка приносит одно полезное значение, а остальное место в кэше тратится впустую. Отсюда — характерная разница между алгоритмами, одинаковыми по числу операций, но разными по схеме доступа.
Что происходит при промахе
Промах — это ситуация, когда нужного значения в кэше нет. Ядро отправляет запрос дальше по иерархии, вплоть до контроллера памяти. Внеочередное исполнение частично прячет ожидание: пока один запрос в пути, ядро выполняет независимые инструкции. Но окно переупорядочивания конечно, и если следующая инструкция зависит от загружаемого значения, конвейер встаёт.
Отдельно работает предвыборка. Аппаратный блок отслеживает регулярные шаблоны обращений и заранее подтягивает строки, которые, по его расчёту, понадобятся. При линейном проходе по массиву предвыборка почти полностью маскирует задержку. При обходе связного списка или дерева, где следующий адрес известен только после чтения текущего узла, предсказать нечего — цепочка зависимых промахов идёт последовательно, и каждый оплачивается полной задержкой памяти.
Когда объём кэша решает
Выигрыш появляется тогда, когда рабочий набор данных — та часть, к которой программа обращается активно в данный момент, — начинает помещаться в кэш целиком. Это пороговый эффект, а не плавный. Пока набор вдвое больше объёма, увеличение кэша даёт немного. Как только набор укладывается, доля промахов падает резко, и вместе с ней исчезают простои конвейера.
Типичные кандидаты на такой выигрыш — интерактивные нагрузки с большим количеством мелких объектов и нерегулярными связями между ними: логика игровых сцен, симуляции, обход графов, разбор структур, работа с индексами в базах данных, компиляция. Общая черта — доступ разбросан по памяти, предвыборка помогает слабо, а объём активных данных сопоставим с размером третьего уровня. Именно эта комбинация делает кэш заметным параметром при выборе процессора.
Когда объём не решает ничего
Обратный случай — потоковая обработка больших массивов. Кодирование видео, рендеринг, обработка изображений, линейная алгебра над крупными матрицами читают данные один раз и больше к ним не возвращаются. Кэшировать нечего: строка приходит, используется и вытесняется. Такая нагрузка упирается в пропускную способность памяти, и решает здесь количество каналов и частота модулей, а не объём кэша. Подбирать конфигурацию оперативной памяти в этих сценариях важнее, чем гнаться за кэшем.
Второй случай — задачи, чей рабочий набор мал изначально и укладывается даже в первый уровень. Прирост от увеличения третьего уровня здесь нулевой просто потому, что до него дело не доходит.
Многопоточность и общий уровень
Третий уровень делится между ядрами, и это создаёт эффекты, которых нет в однопоточном исполнении. Несколько активных потоков вытесняют данные друг друга: каждому достаётся не весь объём, а доля, и порог «набор помещается» может быть пройден в обратную сторону при увеличении числа рабочих потоков. Отсюда неочевидная картина, когда добавление потоков даёт прирост меньше линейного не из-за синхронизации, а из-за конкуренции за кэш.
Ещё один механизм — ложное разделение. Если два потока пишут в разные переменные, попавшие в одну строку кэша, строка непрерывно передаётся между ядрами, и каждая запись оплачивается протоколом когерентности. Внешне это выглядит как необъяснимая просадка при росте числа потоков, хотя логически потоки независимы.
Практический вывод: кэш стоит рассматривать не как отдельную цифру в характеристиках, а как параметр, значимость которого целиком определяется схемой доступа к памяти в конкретной нагрузке. Задача с нерегулярным доступом и умеренным рабочим набором отзовётся на увеличение объёма, потоковая обработка крупных массивов — нет.