EnglishРусский Map

Почему padding кэш-линий делают в 128 байт при размере линии 64 байта

title
Почему padding кэш-линий делают в 128 байт при размере линии 64 байта
type
summary
summary
Иван Болдырев бенчмаркает выравнивание атомиков по 64 и 128 байт: эффект виден на Skylake, но не на Ice Lake и M1
tags
performance, concurrency, microarchitecture, rust
created
2026-07-23
updated
2026-09-14
lang
ru
source_updated
2026-09-14
translated
2026-09-14
translator
lllm/antigravity/gemini-3.7-flash-medium

Два атомика на одной кэш-линии заставляют владеющие ими ядра играть в пинг-понг: протоколы когерентности работают на уровне кэш-линий, а не отдельных адресов, поэтому каждая запись в любую из переменных делает всю линию "грязной" для обоих ядер. Стандартное решение - разнести их с помощью padding'а, а стандартный вопрос - на какое расстояние. Кэш-линии в x86_64 имеют размер 64 байта, и обе книги - Rust Atomics and Locks и Performance Analysis and Tuning on Modern CPUs Бахвалова - советуют брать именно размер кэш-линии. Тем не менее crossbeam-utils и folly от Facebook выравнивают до 128 байт. Иван Болдырев решил выяснить, даёт ли этот лишний 64-байтный довесок хоть какой-то измеримый выигрыш.

Заявленная причина - пространственный prefetcher (spatial prefetcher): начиная с Sandy Bridge, prefetcher L2 у Intel умеет подтягивать кэш-линии соседними парами. Это не делает эффективный размер линии равным 128 байтам, но означает, что запись в строку N может затянуть строку N+1 в кэш другого ядра и породить contention, от которого 64-байтный pad не защищает. Поведение управляется поканальным MSR с названиями вроде "Adjacent Cache Line Prefetcher Disable" или "L2 Adjacent Cache Line Prefetcher Disable" - проверить его перед бенчмаркингом в этой области полезно, если машина вообще позволяет его прочитать.

Как проявить эффект

Попытка запустить два потока, каждый из которых долбит свой атомик, эффекта не даёт: как только каждая линия оседает в кэше своего ядра, трафик MESI прекращается. Сработал вектор из SIZE записей, каждая из которых содержит атомики add и sub, разделённые обёрткой CachePadded<T> с выравниванием на 64 или 128 байт. Один поток идёт по вектору и инкрементирует каждый add, второй - идёт и инкрементирует каждый sub, с упорядочиванием AcqRel, всего миллиард операций.

Две детали делают цифры надёжными. Внутренний цикл предельно прост: общее число операций равно SIZE * floor(N/SIZE), а не строго N, что при N = 1e9 и небольшом SIZE не имеет значения. И оба потока привязаны к ядрам: к ядру 0 и ядру 2 - ядро 1 обычно является соседним hyperthread'ом для ядра 0 и делит с ним кэши, что стёрло бы измеряемый эффект.

Результаты

На AWS c5d.4xlarge (Xeon Platinum 8124M, Skylake) разница реальна, хотя по среднему значению скромна, а по дисперсии велика:

SIZE align 64 align 128
1 7.426 s ± 0.001 7.427 s ± 0.001
2 5.464 s ± 0.002 5.349 s ± 0.001
3 5.475 s ± 0.093 5.249 s ± 0.002
5 5.696 s ± 0.247 5.363 s ± 0.002
7 5.684 s ± 0.201 5.137 s ± 0.001
16 6.943 s ± 0.281 6.646 s ± 0.001

Разрыв по среднему - несколько процентов. Разрыв по стандартному отклонению - два порядка: 0.2 с против 0.001 с при SIZE=7. Это более полезный вывод: вызванный prefetcher'ом трафик когерентности возникает в непредсказуемом порядке и занимает непредсказуемое время, поэтому у варианта с 64 байтами есть длинный хвост распределения, которого у 128-байтного варианта попросту нет. Для чувствительных к задержкам задач (в статье упоминается HFT) именно ради купирования хвоста padding изначально и делался.

SIZE=1 не показывает никакой разницы, что согласуется со случаем "два потока, два атомика", где воспроизвести эффект не удалось.

Где эффект не воспроизводится

VPS на Digital Ocean не показал ничего стабильного ни в одной из конфигураций, с большими отклонениями во всех тестах - автор предполагает, что prefetcher смежных линий там просто выключен.

На AWS c6i.4xlarge (Xeon Platinum 8375C, Ice Lake) между 64 и 128 байтами разницы практически нет, а отклонения малы в обоих случаях. Что-то в более новом prefetcher'е ломает именно этот бенчмарк; статья не претендует на объяснение механизма.

Сюрприз преподнёс Apple Silicon. У M1 действительно 128-байтные кэш-линии, поэтому на нём эффект должен был проявиться резче, чем на любом процессоре Intel, - но этого не происходит. SIZE=3 выполняется за 2.391 с при align 64 и за 2.425 с при align 128, то есть с большим padding'ом результат даже чуть хуже. Автор не смог этого объяснить и приводит ссылку на независимое подтверждение того же нулевого результата.

Честный итог таков: конвенция о 128 байтах имеет смысл на процессорах Intel времён Skylake, главным образом ради хвостовых задержек, а не пропускной способности, и не подтверждается на всём остальном железе, до которого автор смог добраться. Это неплохой аргумент в пользу того, чтобы её сохранить (цена вопроса - память, польза изредка реальна), и слабый аргумент в пользу того, чтобы возводить её в ранг правила.

Пост завершается дисклеймером о том, что текст написан человеком, а ИИ использовался только для генерации идей и вычитки.

Связанные страницы

Ещё один пример того, как производительность зависит от факторов ниже уровня исходного кода, наряду с compiler-codegen-luck и huge-pages - в самом коде на Rust ничего не меняется, меняется только расположение байтов в памяти. golang-green-tea-gc упирается в ту же проблему измерений, но с другой стороны: там эффект невидим в счётчиках L3 и проявляется только при наличии метрики L1 MPKI, для чего нужна машина с доступом к событиям L1 через PMU, а не VPS, с которого в этом бенчмарке не удалось снять показания. Атомики, за которые здесь идёт борьба, лежат в основе spinlock или счётчиков очистки в epoch-based-reclamation - в обоих случаях padding используется ровно по этой причине. Протокол когерентности, стоящий за этим пинг-понгом, и условия, при которых другие ядра увидят запись, описаны в write-atomicity. Про оптимизацию горячих путей на уровне инструкций, а не памяти, см. go-bounds-checks-unsafe. Padding работает и в обратную сторону: удаление небольших полей может уменьшить структуру сильнее своего собственного размера, как показано в big-pineapple-dns-cache-layout.