awesome-everything EN
↑ Обратно к восхождению

Производительность

Cache lines и false sharing: когда параллелизм замедляет код

Суть False sharing — когда два thread пишут в разные переменные одной cache line, MESI протокол ping-pong-ит line между cores на 30–50 циклов per ping. Canonical баг ''''сделал parallel, но стало медленнее''''.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Два thread. Каждый пишет в свою переменную. Нет shared state. Нет mutex. Нет contention — по крайней мере, так кажется из кода. Параллельная версия в три раза медленнее однопоточной.

Cache lines: единица transfer

Data не двигается между RAM и cache по одному байту. Единица — cache line (64 байта на x86 и большинстве ARM, 128 байт на некоторых Apple Silicon и ARM server cores).

Читаешь байт на адресе X — CPU грузит байты X до X+63 в cache, все сразу. Поэтому sequential access так быстр: первый read грузит 8 doubles за раз; следующие 7 — бесплатные L1-хиты.

MESI протокол

В multi-core системе каждое ядро имеет свой L1/L2 cache. Чтобы данные были согласованы, hardware использует MESI протокол: каждая cache line находится в одном из четырёх состояний:

  • M (Modified) — только у этого core, изменена (dirty).
  • E (Exclusive) — только у этого core, не изменена.
  • S (Shared) — у нескольких cores, read-only.
  • I (Invalid) — копия устарела, нужно re-fetch.

Когда core A пишет в line, MESI отправляет invalidation всем cores, у которых есть копия. Их line переходит в состояние I. При следующем обращении они должны re-fetch.

False sharing

False sharing случается, когда два thread пишут в разные переменные, случайно находящиеся в одной cache line.

struct Counters {
    int64_t thread0_count;  // байты 0–7
    int64_t thread1_count;  // байты 8–15
    // обе переменные в одной 64-байтной cache line
};

Thread 0 пишет thread0_count, thread 1 пишет thread1_count. С точки зрения кода — нет shared state. С точки зрения MESI — каждая запись invalidates cache другого thread. Line ping-pong-ит между cores:

T=0: Core 0 пишет → line в M (Core 0)
T=1: Core 1 пишет → Core 0's copy становится I → Core 1 re-fetch → 30–50 циклов
T=2: Core 0 пишет снова → Core 1's copy становится I → Core 0 re-fetch → ещё 30–50 циклов
...

Параллельная работа становится serialized ожиданием cache coherency.

Фикс: padding до 64 байт.

struct Counters {
    alignas(64) int64_t thread0_count;  // занимает собственную cache line
    alignas(64) int64_t thread1_count;  // занимает собственную cache line
};

Или padding вручную:

struct Counter {
    int64_t value;
    char padding[56];  // 8 + 56 = 64 байта ровно
};

Struct layout и struct splitting

Cache line behaviour влияет не только на multi-threading. Struct layout критичен и для single-threaded hot loops.

Рассмотрим:

struct Entity {
    float x, y, z;       // 12 байт — горячие, нужны каждый кадр
    char name[64];        // 64 байта — холодные, нужны редко
    float health;         // 4 байта — горячие
    // total: 80 байт = 2 cache lines
};

Hot loop, обрабатывающий 10 000 entity каждый кадр, читает только x, y, z, health — 16 байт. Но загружает 80 байт (2 cache lines) per entity, потому что name засоряет cache.

Struct splitting (field shuffling): разделяй горячие и холодные поля.

struct EntityHot {
    float x, y, z, health;  // 16 байт — 4 элемента per cache line
};
struct EntityCold {
    char name[64];           // редко нужен
};
EntityHot hot_data[N];  // dense array
EntityCold cold_data[N]; // accessed by index

Теперь hot loop грузит 4 EntityHot per cache line вместо 1 Entity per 2 lines. Cache pressure в 8x ниже.

Cache line и MESI числа
Cache line size (x86, ARM)
64 байта
Cache line size (Apple M3)
128 байт
False sharing ping-pong cost
30–50 циклов per ping
L1 hit latency
~1 нс (3–5 циклов)
RAM latency (DDR5)
~70–100 нс
СимптомFalse sharingLock contention
Код выглядит lock-freeДаНет (mutex виден в коде)
Масштабирование threadsХуже с ростом threadsХуже с ростом threads
Инструмент диагностикиperf c2c (cache-line contention)perf lock, mutex profiler
ФиксPadding до 64 байтLock-free структуры, шире scope
Почему это работает

Реальные примеры false sharing: Linux kernel scheduler per-CPU stats — 2017 (фикс: атрибут cacheline_aligned), Go sync.WaitGroup struct (фикс: padding добавлен в 1.13). Это canonical «я сделал алгоритм parallel, но он стал медленнее» баг.

Викторина

Tight цикл сканирует array из 1М структур, каждая содержит 6 полей всего 48 байт. Cache lines — 64 байта. Сколько структур влезает per cache line?

Викторина

У тебя два thread, каждый incrementируя свой счётчик в tight loop. Struct: { int64_t a; int64_t b; }. Thread 0 пишет a, thread 1 пишет b. Оба в одной 64-байтной cache line. Что произойдёт?

Расставь шаги по порядку

Поставь шаги MESI invalidation при false sharing (два cores пишут в разные переменные одной cache line):

  1. 1 Core 0 пишет в переменную A в cache line X
  2. 2 MESI отправляет invalidation message всем другим cores с копией line X
  3. 3 Core 1's копия line X переходит в состояние Invalid
  4. 4 Core 1 пишет в переменную B в той же line X
  5. 5 Core 1 должен сначала re-fetch line X из Core 0 или памяти — 30–50 циклов
  6. 6 Core 0's копия становится Invalid — следующий доступ Core 0 снова ждёт
Вспомните перед уходом
  1. 01
    Объясни, как cache coherency (MESI) создаёт 'false sharing' как multi-thread performance баг, и как детектировать и фиксить.
Итог

Cache line — 64 байта (x86 / большинство ARM): unit of transfer между RAM и cache. В multi-core системе MESI протокол держит согласованность: когда core пишет в line, все other copies становятся Invalid. False sharing — hardware-level баг: два thread пишут в разные переменные, разделяющие одну cache line. MESI ping-pong-ит line между cores на 30–50 циклов per ping, serializing что должно быть параллельным. Фикс: alignas(64) или padding. Struct splitting — то же в single-threaded контексте: выноси холодные поля из hot loops в отдельные arrays.

Связанные уроки
встречается в167
Продолжить восхождение ↑Branch prediction: 10–30 циклов штрафа за неожиданный if
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.