为什么x64上的伪共享对齐应该是128字节
摘要
本文解释了为什么在x64上伪共享对齐应为128字节而不是通常的64字节,这是因为Intel Sandy Bridge的空间预取器会成对加载缓存行,文章提供了推理和一个展示改进的基准测试。
<p><a href="https://lobste.rs/s/no3kkj/why_false_sharing_alignment_should_be_128">评论</a></p>
查看缓存全文
缓存时间: 2026/07/07 10:15
# 伪共享对齐 来源: https://monoid.github.io/posts/false-sharing-alignment/ ## 什么是伪共享 什么是伪共享 (https://en.wikipedia.org/wiki/False_sharing)?当CPU及其核心读取并更新原子变量时,特殊的硬件协议确保了正确性和效率,同时保持每个核心的缓存一致性。这种协调并非按地址进行:这些协议基于缓存行大小大小的块来工作。当两个原子变量恰好位于同一缓存行时会发生什么?例如:
- 一个原子变量数组,每个原子变量由单独的线程使用(例如,每线程计数器);
- 一个带有两个原子指针的队列,写入器更新尾部,读取器更新头部。
当然,你在同一个结构体中声明了两个指针,它们具有相邻的地址!每次更新操作都会使缓存行变脏,并需要CPU之间的协调。CPU本质上是在与该内存块玩乒乓游戏。
## 怎么做
解决方案很简单:通过使原子变量位于不同的缓存行来将它们分开。这通过一些语言相关的对齐指令完成,尽管重要的不是对齐而是间距。
- Rust: 在类型声明上使用 `#[repr(align(N))]`(或使用类型包装器)
- C11: 在类型或变量声明上使用 `_Alignas(N)`
- C++11: 在类型、字段或变量声明上使用 `alignas(N)`
- GCC/Clang: 使用 `__attribute__((aligned(N)))`
*Rust Atomics and Locks*(Mara Bos著)和*Performance Analysis and Tuning on Modern CPUs*(Denis Bakhvalov著)建议使用缓存行大小,对于`x86_64`是64字节。然而,库(`crossbeam-utils` (https://github.com/crossbeam-rs/crossbeam/blob/822eb3abf00094fab5d817538aab42477c62c42a/crossbeam-utils/src/cache_padded.rs#L82-L90),Facebook的`folly` (https://github.com/facebook/folly/blob/4d6b3c7999940ddf2e9c1eb9ed2c9cf3d8c2280f/folly/lang/Align.h#L186-L187))在这里使用128字节。
**为什么?** 原因是自Intel Sandy Bridge架构以来,空间预取器可能会*成对*加载缓存行。这并不会使有效缓存行大小变为128字节,但仍然有它的副作用。此外,这种行为由每个核心的MSR设置控制,称为"Adjacent Cache Line Prefetcher Disable"或"L2 Adjacent Cache Line Prefetcher Disable"。在基准测试前检查它!如果你可以的话。
## 基准测试
我尝试在实际基准测试中重现128字节对齐的改进。这并不容易!仅仅启动两个线程各自更新一个原子变量是不够的:一旦原子变量位于各自的CPU缓存中,似乎不会发生MESI交互。让我们尝试更复杂的东西:一个原子变量向量应该可以解决问题。让我们模拟经典的两指针原子队列:一个用于头部(由读取器递增)的原子变量,以及一个用于尾部(由写入器递增)的原子变量。使用 `#[repr(align(64))]` 或 `#[repr(align(128))]`(在下面的Rust代码中,用于一个类似于 `crossbeam-utils` 的包装器类型)将使原子变量之间相距64或128字节。单个原子变量对可能不足以对内存产生压力;让我们获取一个长度为 `SIZE` 的记录向量。每个线程(共两个)在循环中修改每个记录的 `add` 或 `sub` 字段。线程体看起来有点奇怪:
```rust
for _ in 0..(N / SIZE) {
for elt in addsub {
elt.add.fetch_add(1u64, ORDERING);
}
}
```
你可能注意到操作次数是 `SIZE * floor(N / SIZE)`,不等于 `N`。这使得比较不同的 `SIZE` 更加困难……直到我们注意到差异确实可以忽略不计,因为我们的 `SIZE` 很小而 `N` 很大。但内部循环非常简单,使得基准测试更可靠。
你可以在 https://github.com/monoid/junk/tree/master/false_sharing 获取可运行的代码,但为了方便,下面提供了简化代码。
**源代码**
```rust
use std::ops::Deref;
use std::sync::atomic::{AtomicU64, Ordering};
// 步数。
const N: usize = 1_000_000_000;
const ORDERING: Ordering = Ordering::AcqRel;
// AddSubs 向量的长度。
const SIZE: usize = 8;
// 取消注释以下任意一行。
// #[repr(align(64))]
// #[repr(align(128))]
#[derive(Default, Debug)]
struct CachePadded<T> {
value: T,
}
impl<T> Deref for CachePadded<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.value
}
}
// 两个原子变量并排放置,由上面配置的缓存填充隔开。
#[derive(Default, Debug)]
struct AddSub {
// 由第一个线程更新。
add: CachePadded<AtomicU64>,
// 由第二个线程更新。
sub: CachePadded<AtomicU64>,
}
impl AddSub {
fn get_value(&self) -> u64 {
let add = self.add.load(Ordering::Relaxed);
let sub = self.sub.load(Ordering::Relaxed);
add.wrapping_sub(sub)
}
}
fn main() {
let data = Vec::from_iter(std::iter::repeat_with(|| AddSub::default()).take(SIZE));
let addsub: Box<[AddSub]> = data.into();
let addsub = &*Box::leak(addsub);
let t1 = std::thread::spawn(move || {
#[cfg(target_arch = "x86_64")]
affinity::set_thread_affinity(&[0]).unwrap();
for _ in 0..(N / SIZE) {
for elt in addsub {
elt.add.fetch_add(1u64, ORDERING);
}
}
});
let t2 = std::thread::spawn(move || {
#[cfg(target_arch = "x86_64")]
// 不是1!核心1通常是核心0的同一个物理核心的虚拟核心,共享相同的缓存。
affinity::set_thread_affinity(&[2]).unwrap();
for _ in 0..(N / SIZE) {
for elt in addsub {
elt.sub.fetch_add(1u64, ORDERING);
}
}
});
t1.join().unwrap();
t2.join().unwrap();
for item in &*addsub {
eprintln!("{}", item.get_value());
}
}
```
绑定CPU亲和性使基准测试更具可预测性。它既避免了线程在核心间迁移带来的噪声,也避免了同一核心上线程使用相同缓存的情况。
我编译了一组带有不同参数的二进制文件,然后运行了以下命令:
```bash
for i in $(seq 1 8) 12 16; do
for bits in 64 128; do
FSHARING_SIZE=$i cargo build --release --features cache_line_${bits}
mv target/release/false_sharing ./false_sharing_${i}_${bits}
done
done
hyperfine --warmup 3 --export-json results.json ./false_sharing_*
```
## Digital Ocean
我首先在Digital Ocean的VPS实例上进行基准测试。不幸的是,我无法可靠地重现任何差异。我怀疑Digital Ocean上简单地禁用了相邻预取器。对于128字节和64字节对齐,标准偏差也相当大。
## Amazon Web Services, c5d (Skylake)
我能够在AWS的`c5d.4xlarge`实例(Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz)上可靠地重现差异。而且执行时间并不是64字节和128字节对齐之间的唯一区别!不幸的是,我无法在AWS上分析程序的执行,因为这里大多数硬件计数器都受限。
**CPU信息**
```
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 85
model name : Intel(R) Xeon(R) Platinum 8124M CPU @ 3.00GHz
stepping : 4
microcode : 0x2007006
cpu MHz : 3354.253
cache size : 25344 KB
physical id : 0
siblings : 16
core id : 0
cpu cores : 8
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault pti fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid mpx avx512f avx512dq rdseed adx smap clflushopt clwb avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves ida arat pku ospke
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit mmio_stale_data retbleed gds bhi spectre_v2_user its
bogomips : 6000.01
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:
```
## 结果
| 大小 | 对齐64, 时间 (均值 ± σ) | 对齐128, 时间 (均值 ± σ) |
|------|--------------------------|--------------------------|
| 1 | 7.426 s ± 0.001 s | 7.427 s ± 0.001 s |
| 2 | 5.464 s ± 0.002 s | 5.349 s ± 0.001 s |
| 3 | 5.475 s ± 0.093 s | 5.249 s ± 0.002 s |
| 4 | 5.541 s ± 0.092 s | 5.420 s ± 0.001 s |
| 5 | 5.696 s ± 0.247 s | 5.363 s ± 0.002 s |
| 6 | 5.609 s ± 0.239 s | 5.396 s ± 0.001 s |
| 7 | 5.684 s ± 0.201 s | 5.137 s ± 0.001 s |
| 8 | 6.800 s ± 0.241 s | 6.679 s ± 0.000 s |
| 12 | 6.258 s ± 0.064 s | 6.189 s ± 0.001 s |
| 16 | 6.943 s ± 0.281 s | 6.646 s ± 0.001 s |
128字节对齐不仅快了百分之几,还显示了**更一致的计时**,这对于实时和延迟敏感的应用(如HFT)可能至关重要。原因很明显:预取器引发的缓存一致性在64字节对齐中以不可预测的顺序发生,且耗时不可预测,而128字节对齐消除了这种争用。
**原始结果, AWS c5d.4xlarge**
```
$ hyperfine --warmup 3 --export-json results.json ./false_sharing_*
./false_sharing_12_128
Time (mean ± σ): 6.189 s ± 0.001 s [User: 12.373 s, System: 0.001 s]
Range (min … max): 6.188 s … 6.190 s 10 runs
./false_sharing_12_64
Time (mean ± σ): 6.258 s ± 0.064 s [User: 12.497 s, System: 0.002 s]
Range (min … max): 6.206 s … 6.415 s 10 runs
./false_sharing_16_128
Time (mean ± σ): 6.646 s ± 0.001 s [User: 13.285 s, System: 0.002 s]
Range (min … max): 6.645 s … 6.647 s 10 runs
./false_sharing_16_64
Time (mean ± σ): 6.943 s ± 0.281 s [User: 13.878 s, System: 0.001 s]
Range (min … max): 6.649 s … 7.378 s 10 runs
./false_sharing_1_128
Time (mean ± σ): 7.427 s ± 0.001 s [User: 14.845 s, System: 0.001 s]
Range (min … max): 7.426 s … 7.428 s 10 runs
./false_sharing_1_64
Time (mean ± σ): 7.426 s ± 0.001 s [User: 14.843 s, System: 0.002 s]
Range (min … max): 7.424 s … 7.427 s 10 runs
./false_sharing_2_128
Time (mean ± σ): 5.349 s ± 0.001 s [User: 10.690 s, System: 0.001 s]
Range (min … max): 5.348 s … 5.349 s 10 runs
./false_sharing_2_64
Time (mean ± σ): 5.464 s ± 0.002 s [User: 10.809 s, System: 0.001 s]
Range (min … max): 5.463 s … 5.468 s 10 runs
./false_sharing_3_128
Time (mean ± σ): 5.249 s ± 0.002 s [User: 10.492 s, System: 0.001 s]
Range (min … max): 5.247 s … 5.251 s 10 runs
./false_sharing_3_64
Time (mean ± σ): 5.475 s ± 0.093 s [User: 10.923 s, System: 0.001 s]
Range (min … max): 5.414 s … 5.726 s 10 runs
./false_sharing_4_128
Time (mean ± σ): 5.420 s ± 0.001 s [User: 10.832 s, System: 0.001 s]
Range (min … max): 5.419 s … 5.421 s 10 runs
./false_sharing_4_64
Time (mean ± σ): 5.541 s ± 0.092 s [User: 11.073 s, System: 0.001 s]
Range (min … max): 5.439 s … 5.757 s 10 runs
./false_sharing_5_128
Time (mean ± σ): 5.363 s ± 0.002 s [User: 10.717 s, System: 0.002 s]
Range (min … max): 5.359 s … 5.367 s 10 runs
./false_sharing_5_64
Time (mean ± σ): 5.696 s ± 0.247 s [User: 11.387 s, System: 0.001 s]
Range (min … max): 5.361 s … 6.235 s 10 runs
./false_sharing_6_128
Time (mean ± σ): 5.396 s ± 0.001 s [User: 10.785 s, System: 0.001 s]
Range (min … max): 5.395 s … 5.397 s 10 runs
./false_sharing_6_64
Time (mean ± σ): 5.609 s ± 0.239 s [User: 11.210 s, System: 0.001 s]
Range (min … max): 5.371 s … 6.012 s 10 runs
./false_sharing_7_128
Time (mean ± σ): 5.137 s ± 0.001 s [User: 10.265 s, System: 0.001 s]
Range (min … max): 5.135 s … 5.138 s 10 runs
./false_sharing_7_64
Time (mean ± σ): 5.684 s ± 0.201 s [User: 11.362 s, System: 0.001 s]
Range (min … max): 5.343 s … 6.057 s 10 runs
./false_sharing_8_128
Time (mean ± σ): 6.679 s ± 0.000 s [User: 13.136 s, System: 0.001 s]
Range (min … max): 6.678 s … 6.679 s 10 runs
./false_sharing_8_64
Time (mean ± σ): 6.800 s ± 0.241 s [User: 13.590 s, System: 0.002 s]
Range (min … max): 6.684 s … 7.473 s 10 runs
./false_sharing_12_128
Time (mean ± σ): 6.189 s ± 0.000 s [User: 12.375 s, System: 0.001 s]
Range (min … max): 6.189 s … 6.190 s 10 runs
Summary
./false_sharing_7_128 ran
1.02 ± 0.00 times faster than ./false_sharing_3_128
1.04 ± 0.00 times faster than ./false_sharing_2_128
1.04 ± 0.00 times faster than ./false_sharing_5_128
1.05 ± 0.00 times faster than ./false_sharing_6_128
1.06 ± 0.00 times faster than ./false_sharing_4_128
1.06 ± 0.00 times faster than ./false_sharing_2_64
1.07 ± 0.02 times faster than ./false_sharing_3_64
1.08 ± 0.02 times faster than ./false_sharing_4_64
1.09 ± 0.05 times faster than ./false_sharing_6_64
1.11 ± 0.04 times faster than ./false_sharing_7_64
1.11 ± 0.05 times faster than ./false_sharing_5_64
1.20 ± 0.00 times faster than ./false_sharing_12_128
1.20 ± 0.00 times faster than ./false_sharing_12_128
1.22 ± 0.01 times faster than ./false_sharing_12_64
1.29 ± 0.00 times faster than ./false_sharing_16_128
1.30 ± 0.00 times faster than ./false_sharing_8_128
1.32 ± 0.05 times faster than ./false_sharing_8_64
1.35 ± 0.05 times faster than ./false_sharing_16_64
1.45 ± 0.00 times faster than ./false_sharing_1_64
1.45 ± 0.00 times faster than ./false_sharing_1_128
```
## Amazon Web Services, c6i (Ice Lake)
令我惊讶的是,`c6i.4xlarge`实例(Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz,Ice Lake)在64字节和128字节情况下几乎没有差异,而标准偏差始终非常小。我只能猜测这里的预取器更复杂,可能需要另一种方法来重现问题。
| 大小 | 对齐64, 时间 (均值 ± σ) | 对齐128, 时间 (均值 ± σ) |
|------|--------------------------|--------------------------|
| 1 | 6.073 s ± 0.001 s | 6.073 s ± 0.001 s |
| 2 | 5.624 s ± 0.004 s | 5.608 s ± 0.003 s |
| 5 | 5.539 s ± 0.000 s | 5.546 s ± 0.000 s |
| 12 | 5.512 s ± 0.000 s | 5.512 s ± 0.000 s |
**原始结果, AWS c6i.4xlarge**
```
$ hyperfine --warmup 3 --export-json c6i.json ./false_sharing_*
./false_sharing_12_128
Time (mean ± σ): 5.512 s ± 0.000 s [User: 11.012 s, System: 0.001 s]
Range (min … max): 5.512 s … 5.513 s 10 runs
./false_sharing_12_64
Time (mean ± σ): 5.512 s ± 0.000 s [User: 11.011 s, System: 0.001 s]
Range (min … max): 5.512 s … 5.513 s 10 runs
./false_sharing_16_128
Time (mean ± σ): 5.506 s ± 0.000 s [User: 10.998 s, System: 0.001 s]
Range (min … max): 5.505 s … 5.507 s 10 runs
Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.
./false_sharing_16_64
Time (mean ± σ): 5.506 s ± 0.000 s [User: 10.999 s, System: 0.001 s]
Range (min … max): 5.506 s … 5.507 s 10 runs
./false_sharing_1_128
Time (mean ± σ): 6.073 s ± 0.001 s [User: 12.136 s, System: 0.001 s]
Range (min … max): 6.071 s … 6.074 s 10 runs
./false_sharing_1_64
Time (mean ± σ): 6.073 s ± 0.001 s [User: 12.136 s, System: 0.001 s]
Range (min … max): 6.071 s … 6.075 s 10 runs
./false_sharing_2_128
Time (mean ± σ): 5.608 s ± 0.003 s [User: 11.205 s, System: 0.001 s]
Range (min … max): 5.604 s … 5.612 s 10 runs
./false_sharing_2_64
Time (mean ± σ): 5.624 s ± 0.004 s [User: 11.236 s, System: 0.001 s]
Range (min … max): 5.617 s … 5.630 s 10 runs
./false_sharing_3_128
Time (mean ± σ): 5.574 s ± 0.001 s [User: 11.135 s, System: 0.001 s]
Range (min … max): 5.572 s … 5.576 s 10 runs
./false_sharing_3_64
Time (mean ± σ): 5.577 s ± 0.003 s [User: 11.130 s, System: 0.001 s]
Range (min … max): 5.571 s … 5.580 s 10 runs
./false_sharing_4_128
Time (mean ± σ): 5.560 s ± 0.000 s [User: 11.109 s, System: 0.001 s]
Range (min … max): 5.560 s … 5.561 s 10 runs
./false_sharing_4_64
Time (mean ± σ): 5.560 s ± 0.000 s [User: 11.099 s, System: 0.001 s]
Range (min … max): 5.560 s … 5.561 s 10 runs
./false_sharing_5_128
Time (mean ± σ): 5.546 s ± 0.000 s [User: 11.066 s, System: 0.001 s]
Range (min … max): 5.545 s … 5.547 s 10 runs
./false_sharing_5_64
Time (mean ± σ): 5.539 s ± 0.000 s [User: 11.064 s, System: 0.001 s]
Range (min … max): 5.539 s … 5.540 s 10 runs
./false_sharing_6_128
Time (mean ± σ): 5.569 s ± 0.003 s [User: 11.124 s, System: 0.001 s]
Range (min … max): 5.565 s … 5.573 s 10 runs
./false_sharing_6_64
Time (mean ± σ): 5.567 s ± 0.001 s [User: 11.121 s, System: 0.001 s]
Range (min … max): 5.566 s … 5.569 s 10 runs
./false_sharing_7_128
Time (mean ± σ): 5.563 s ± 0.001 s [User: 11.113 s, System: 0.001 s]
Range (min … max): 5.563 s … 5.564 s 10 runs
./false_sharing_7_64
Time (mean ± σ): 5.562 s ± 0.001 s [User: 11.111 s, System: 0.001 s]
Range (min … max): 5.561 s … 5.563 s 10 runs
./false_sharing_8_128
Time (mean ± σ): 5.556 s ± 0.001 s [User: 11.098 s, System: 0.001 s]
Range (min … max): 5.554 s … 5.557 s 10 runs
./false_sharing_8_64
Time (mean ± σ): 5.556 s ± 0.001 s [User: 11.098 s, System: 0.001 s]
Range (min … max): 5.554 s … 5.557 s 10 runs
./false_sharing_12_128
Time (mean ± σ): 5.512 s ± 0.000 s [User: 11.012 s, System: 0.001 s]
Range (min … max): 5.512 s … 5.513 s 10 runs
./false_sharing_12_64
Time (mean ± σ): 5.512 s ± 0.000 s [User: 11.011 s, System: 0.001 s]
Range (min … max): 5.512 s … 5.513 s 10 runs
```
相似文章
Performance impact of Alignment
This technical blog post explains the performance impact of memory alignment in SIMD vectorization, covering architectures with strict alignment requirements, cacheline crossing, and the behavior of modern CPUs.
Windows堆栈限制检查回顾,后续
Raymond Chen跟进了他之前关于ARM64堆栈限制检查的文章,指出了堆栈探测函数中x15寄存器的非常规使用细节,并比较了多个架构的寄存器使用。
在Rust中的缓存感知数据布局:字段分区、伪共享与128字节规则
这篇博客文章解释了Rust中针对多线程结构的缓存感知数据布局,涵盖了字段分区和避免伪共享的128字节规则,并以一个SPSC环形缓冲区为例。
80386 早期启动内存访问
本文解释了 Intel 80386 中的早期启动内存访问技术,该技术通过将地址生成与前一条指令的最后一个周期重叠来隐藏内存延迟。文章描述了该技术在 z386 FPGA 核心中的实现,达到了 ao486 级别的性能,并在 Doom FPS 上提升了 39%。
每个字节都很重要
本文通过Java和C语言的示例,阐述了理解CPU缓存行与数据结构布局对编程性能优化的重要性,讨论了多余字节的开销以及结构体数组与数组结构体之间的权衡。