Архитектура компьютера
Многоядерные процессоры: когерентность кэша
Вы распараллелили код на 8 ядер и ожидаете ускорение в 8 раз. Реальность: 1.2×. Причина - два потока случайно пишут в переменные, лежащие рядом в памяти. Кэш-когерентность превращает параллелизм в кошмар, если не знать о ней.
- False sharing - причина деградации многопоточных серверов
- MESI протокол в каждом современном x86/ARM
- Java ConcurrentHashMap использует разные строки кэша для бакетов
- Linux kernel: per-CPU переменные специально выровнены по кэш-линиям
Проблема когерентности кэша
**Сценарий:** Core 0 кэшировал переменную x=5. Core 1 изменил x на 10 в своём кэше. Теперь Core 0 читает x - и видит 5, хотя оно должно быть 10. Это **проблема когерентности кэша**.
**Memory Ordering:** Даже если кэш когерентен, CPU и компилятор могут переупорядочивать инструкции. Нужны явные барьеры памяти (memory fences) или атомарные операции с memory ordering.
Core A записал x=10. Core B читает x и видит 5. В чём причина?
MESI: протокол когерентности
**MESI** - протокол когерентности кэша. Каждая кэш-линия (обычно 64 байта) находится в одном из 4 состояний: **M**odified (изменена), **E**xclusive (эксклюзивна), **S**hared (разделена), **I**nvalid (недействительна).
**Snooping vs Directory:** Snooping (прослушивание шины) работает на процессорах с 2-16 ядрами. Directory-based когерентность используется в NUMA-системах с сотнями ядер - каждый регион памяти имеет directory, хранящий информацию о копиях.
Core 0 имеет кэш-линию в состоянии S. Core 0 хочет записать в неё. Что произойдёт?
False Sharing: скрытый убийца производительности
**False sharing** - два ядра пишут в разные переменные, но они лежат в одной кэш-линии (64 байта). MESI инвалидирует всю строку при каждой записи, хотя ядра пишут в разные данные.
**Практика:** Perf stat / cachegrind показывают cache misses. Intel VTune визуализирует false sharing. В Java: @Contended аннотация автоматически добавляет padding. В Rust: #[repr(align(64))].
Если два потока пишут в разные переменные, они не мешают друг другу. False sharing - это про то же самое слово или поле, не про разные данные.
False sharing возникает именно потому что переменные разные, но лежат в одной кэш-линии (обычно 64 байта). MESI инвалидирует всю линию при каждой записи, и независимые потоки выстраиваются в очередь на ping-pong кэш-линий - производительность падает в 10-100 раз.
Модель «разные переменные = независимые операции» верна на уровне языка, но кэш работает не байтами, а линиями. Два счётчика, объявленные подряд в struct, попадают в одну линию и блокируют друг друга. Padding до 64 байт или @Contended в Java/cache-line-aligned в C++ - не оптимизация, а условие корректной масштабируемости на многоядерных системах.
Два потока пишут в разные переменные, но производительность ужасная. Вероятная причина?
Ключевые идеи
- Когерентность кэша: все ядра видят одинаковое значение переменной
- MESI: 4 состояния кэш-линии - Modified, Exclusive, Shared, Invalid
- Запись в Shared строку → инвалидация всех копий у других ядер
- False sharing: запись в разные переменные одной кэш-линии - скрытый bottleneck
- Решение: выровнять горячие переменные по 64-байтным кэш-линиям
Связанные темы
Когерентность кэша - фундамент параллельного программирования.
- Кэш — MESI управляет состоянием каждой кэш-линии
- ARM vs x86 — ARM имеет более слабую модель памяти чем x86
Вопросы для размышления
- Почему NUMA архитектура усложняет проблему когерентности по сравнению с UMA?
- Как атомарные операции (CAS) используют MESI-протокол?
- Почему volatile в Java/C++ недостаточно для корректной многопоточности?
Связанные уроки
- arch-09-cache — Cache - ключевой ресурс, который должен быть когерентен между ядрами
- arch-06-pipelining — Суперскалярность каждого ядра - контекст для понимания NUMA
- arch-15-gpu-architecture — GPU - другая модель massive parallelism с другими трейдоффами
- alg-01-big-o — Закон Амдала - это Big O для параллельного ускорения
- os-05-sync