Why false sharing alignment should be 128 bytes on x64
Why false sharing alignment should be 128 bytes on x64
为什么在 x64 架构上伪共享对齐应设为 128 字节
What is false sharing?
什么是伪共享?
When CPUs and their cores read and update atomic variables, special hardware protocols make it correct and efficient, while keeping each core’s caches consistent. The coordination doesn’t happen per-address: these protocols work on cache-line-sized chunks. 当 CPU 及其核心读取和更新原子变量时,特殊的硬件协议会确保操作的正确性和高效性,同时保持每个核心缓存的一致性。这种协调并非基于单个地址进行:这些协议是以缓存行(cache-line)大小的数据块为单位工作的。
What happens when two atomic variables happen to reside on the same cache line? For example: 当两个原子变量恰好位于同一个缓存行时会发生什么?例如:
- an array of atomics where each atomic is used by a separate thread (e.g. per-thread counters);
- 一个原子变量数组,其中每个原子变量由不同的线程使用(例如:线程计数器);
- a queue with two atomic pointers where a writer updates the tail and a reader updates the head. Of course, you declare both pointers in the same struct and they have adjacent addresses!
- 一个包含两个原子指针的队列,其中写入者更新尾部,读取者更新头部。当然,你会在同一个结构体中声明这两个指针,它们也就拥有了相邻的地址!
Each update operation makes the cache line dirty and requires coordination between CPUs. The CPUs are essentially playing ping pong with the chunk of memory. 每次更新操作都会使缓存行变脏(dirty),并需要 CPU 之间进行协调。本质上,CPU 正在对这块内存进行“乒乓”操作。
What to do
该怎么做
The solution is simple: separate the atomics by making them reside in different cache lines. It is done with some language-dependent alignment directive, though it is not the alignment but the spacing that matters. 解决方案很简单:通过让原子变量位于不同的缓存行来将它们隔开。这可以通过特定语言的对齐指令来实现,尽管真正起作用的是间距而非对齐本身。
- Rust:
#[repr(align(N))]on the type declaration (or use a type wrapper) - Rust: 在类型声明上使用
#[repr(align(N))](或使用类型包装器) - C11:
_Alignas(N)on type or variable declaration - C11: 在类型或变量声明上使用
_Alignas(N) - C++11:
alignas(N)on type, field or variable declaration - C++11: 在类型、字段或变量声明上使用
alignas(N) - GCC/Clang:
__attribute__((aligned(N))) - GCC/Clang: 使用
__attribute__((aligned(N)))
Both Rust Atomics and Locks by Mara Bos and Performance Analysis and Tuning on Modern CPUs by Denis Bakhvalov recommend using cache line size, which is 64 bytes for x86_64. However, libraries (crossbeam-utils, Facebook’s folly) use 128 bytes here. Why? Mara Bos 所著的《Rust Atomics and Locks》和 Denis Bakhvalov 所著的《Performance Analysis and Tuning on Modern CPUs》都建议使用缓存行大小,对于 x86_64 来说是 64 字节。然而,一些库(如 crossbeam-utils、Facebook 的 folly)在这里使用了 128 字节。为什么?
The reason is that since the Intel Sandy Bridge architecture, the spatial prefetcher may load cache lines in pairs. It doesn’t make the effective cache line size equal to 128 bytes, but still has its side-effects. 原因在于,自 Intel Sandy Bridge 架构以来,空间预取器(spatial prefetcher)可能会成对加载缓存行。这并没有使有效的缓存行大小变为 128 字节,但仍然会产生副作用。
Moreover, this behavior is controlled by a per-core MSR setting called either “Adjacent Cache Line Prefetcher Disable” or “L2 Adjacent Cache Line Prefetcher Disable”. Check it before benchmarking! If you can. 此外,这种行为由每个核心的 MSR 设置控制,称为“Adjacent Cache Line Prefetcher Disable”或“L2 Adjacent Cache Line Prefetcher Disable”。在进行基准测试前请检查它!如果可以的话。
Benchmark
基准测试
I tried to reproduce the 128-byte alignment improvement on a real benchmark. It wasn’t easy! Just starting 2 threads updating an atomic each is not enough: once the atomics are in the respective CPU caches, no MESI interaction seems to be done. Let’s try something more involved: a vector of atomics should do the trick. 我尝试在真实的基准测试中复现 128 字节对齐带来的性能提升。这并不容易!仅仅启动两个线程分别更新一个原子变量是不够的:一旦原子变量进入各自的 CPU 缓存,似乎就不会发生 MESI 交互。让我们尝试更复杂的方法:使用原子变量向量应该能奏效。
Let’s imitate a classical two-pointer atomic queue: an atomic for head being incremented by reader, and an atomic for tail incremented by writer. Using either #[repr(align(64))] or #[repr(align(128))] in the Rust code below for a wrapper type similar to crossbeam-utils will place the atomics either 64 or 128 bytes apart.
让我们模拟一个经典的双指针原子队列:一个由读取者递增的头部原子变量,以及一个由写入者递增的尾部原子变量。在下面的 Rust 代码中,为类似于 crossbeam-utils 的包装类型使用 #[repr(align(64))] 或 #[repr(align(128))],将使原子变量分别间隔 64 或 128 字节。
(Code omitted for brevity) (代码从略)
Pinning makes the benchmark more predictable. It avoids both noise from a thread migrating from a core to core and the same-core situation when threads use the same cache. 绑定核心(Pinning)使基准测试更具可预测性。它既避免了线程在核心间迁移带来的噪声,也避免了线程使用相同缓存时的同核心竞争情况。
Digital Ocean
Digital Ocean
I started by benchmarking at a Digital Ocean VPS instance. Unfortunately, I wasn’t able to reproduce anything reliably. I suspect that the Adjacent Prefetcher is simply disabled on Digital Ocean. Standard deviation was also quite large for both 128 and 64-byte alignment. 我首先在 Digital Ocean 的 VPS 实例上进行了基准测试。遗憾的是,我无法可靠地复现任何结果。我怀疑 Digital Ocean 禁用了相邻预取器。此外,128 字节和 64 字节对齐的标准差都相当大。