Процессор умеет выполнять миллиарды операций в секунду, но его узкое место — память. Если бы он каждый раз брал данные напрямую из оперативки, ядро простаивало бы сотни тактов. Чтобы убрать это «бутылочное горлышко», рядом с процессором сделали кэш — маленькую, но сверхбыструю память. Она дорога в производстве, поэтому её мало, зато она работает почти мгновенно.
Кэш строится как иерархия. L1 — десятки килобайт, задержка 1–3 такта. L2 — сотни килобайт, задержка 10–15 тактов. L3 — мегабайты, задержка 30–50 тактов. Дальше идёт оперативная память — DRAM. Она отвечает через сотни тактов, что в сотни раз медленнее. Для процессора, работающего на гигагерцах, это разница в десятки или даже сотни наносекунд.
Почему так? Дело в устройстве. Кэш делают на SRAM (Static RAM). Здесь каждый бит хранится в триггере, данные не «убегают», и доступ занимает наносекунды. Но такая ячейка большая и дорогая, поэтому много SRAM не поставить. Оперативку делают на DRAM (Dynamic RAM). В ней каждый бит хранится как заряд в крошечном конденсаторе. Заряд утекает, память нужно обновлять каждые миллисекунды. Чтобы прочитать данные, контроллер открывает целую строку ячеек, считывает её в буфер, а потом закрывает. Из-за этого DRAM компактна и дешёвая, но медленная.
Работа кэша сводится к попаданиям и промахам. Попадание (hit) — данные уже в кэше, процессор их получает за наносекунды. Промах (miss) — данных нет, приходится идти глубже: сначала в L2, потом в L3 и только потом в DRAM. Каждый промах стоит десятки или сотни тактов.
Чтобы показать на примере, можно представить игру с картой мира. Вся карта хранится в оперативке. Герой видит только ближайший кусок — он загружен в L1. Когда он выходит за пределы, игра берёт данные из L2 или L3. Если и там нет, идёт в DRAM — и экран подтормаживает. Попадание — это когда нужный кусок уже есть, промах — когда его нужно подтянуть заново.
Эффективность кэша объясняется принципами локальности. Временная локальность: если данные использовались недавно, они, скорее всего, понадобятся снова. Пространственная локальность: если нужен один элемент, значит, скоро понадобятся и соседи. Поэтому процессор загружает не байт, а целую строку кэша — обычно 64 байта. Из-за этого массивы работают быстрее списков: рядом лежащие элементы подтягиваются одним блоком.
Современные процессоры используют предвыборку (prefetch). Если программа идёт по данным последовательно, процессор заранее подгружает следующие строки. Поэтому чтение файла подряд идёт быстрее, чем случайные выборки по разным адресам.
Есть и другие нюансы. Ассоциативность кэша позволяет одной строке памяти храниться в нескольких местах, иначе часто используемые данные вытесняли бы друг друга и вызывали конфликты. В многоядерных системах работает когерентность: если одно ядро изменило значение в своём кэше, остальные должны увидеть новые данные. Для этого есть протоколы согласованности, но они создают нагрузку на шину. В серверах это проявляется как «false sharing» — несколько потоков пишут в разные переменные, оказавшиеся в одной строке кэша, и система замедляется.
Кэш нужен не только для данных, но и для инструкций. Если программа большая и прыгает по разным местам кода, промахи случаются и в кэше инструкций, что тоже замедляет выполнение.
Есть ещё один важный элемент — TLB, буфер трансляции адресов. Это кэш для таблиц страниц виртуальной памяти. Если в TLB случается промах, процессор вынужден искать адрес в таблице в DRAM, и задержка становится ещё больше.
Когда речь заходит о кэше, важно понимать не только теорию, но и то, как она проявляется в работе системы. На практике всё сводится к тому, попадают ли данные в быстрый кэш или приходится ждать ответ от медленной памяти. Если индекс базы данных помещается в L3, запросы выполняются очень быстро. Как только индекс становится больше кэша, промахов резко больше, и база работает медленнее. При последовательном просмотре логов чтение идёт быстро, а если прыгать по файлу случайным образом, система начинает тянуть новые блоки из RAM, и скорость падает. Если контейнер или виртуальная машина активно использует память, она может вытеснять данные соседних сервисов из L3, и их работа замедляется, хотя они напрямую не связаны.
Если перевести задержки в цифры, получится лестница. L1 отвечает за 1 нс, L2 за 3–5 нс, L3 за 10–15 нс, DRAM за 50–100 нс. Разница между L1 и DRAM — сотни раз. Для процессора это значит, что за время ожидания одного слова из оперативки он мог выполнить сотни инструкций.
Кэш — посредник между быстрым процессором и медленной оперативкой. Попадания позволяют ядру работать без простоев, промахи сразу бьют по производительности. Локальность и предвыборка помогают держать эффективность, ассоциативность и когерентность обеспечивают правильность. Это значит: кэш напрямую влияет на скорость баз, работу сервисов под нагрузкой и поведение системы в многопоточности.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.