Архитектура компьютера

Многоядерные процессоры: когерентность кэша

Вы распараллелили код на 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
Многоядерные процессоры: когерентность кэша

0

1

Войти