🔑 为什么“明明没共享数据”也会变慢:False Sharing(伪共享)
多线程程序变慢时,很多人先怀疑锁、系统调用或者算法复杂度,但有一种更阴的情况是:线程之间逻辑上没有竞争,代码看起来也各写各的,性能却还是突然掉下去。这 often 不是“数据共享”,而是 cache line 共享,也就是所谓的 False Sharing。
现代 CPU 不会按“一个变量”来缓存内存,而是按一整块来搬运,常见大小是 64 字节,这一块就叫 cache line。假设两个线程分别修改
这就是它名字里“false”的意思:表面上看像共享写入冲突,实际上程序语义上根本没有在争同一份数据。设计上,CPU 必须按 cache line 管理,因为按字节追踪一致性成本太高,硬件根本扛不住。所以这不是 CPU “笨”,而是工程上的现实取舍:用较粗粒度的缓存块换取整体吞吐,但代价就是程序布局不当时会踩坑。
最常见的坑出现在计数器、统计数组、任务队列元数据里。比如你开 8 个线程,每个线程维护自己的计数,本来以为不会互相影响,于是写成一个连续数组
但也别走到另一个极端。不是所有相邻变量都该强行 padding 成 64 字节,那会浪费内存,还可能让 cache locality 变差。关键判断标准很简单:如果多个线程会高频写,并且这些数据可能挨在一起,就该警觉;如果只是只读共享,或者很少修改,False Sharing 往往不是主因。
🧪 课后一题:有 4 个线程分别不停执行
💡 易混淆点:False Sharing 不是“多个线程读写同一个变量”,而是“多个线程写不同变量,但这些变量落在同一个 cache line 里”。
#CS
多线程程序变慢时,很多人先怀疑锁、系统调用或者算法复杂度,但有一种更阴的情况是:线程之间逻辑上没有竞争,代码看起来也各写各的,性能却还是突然掉下去。这 often 不是“数据共享”,而是 cache line 共享,也就是所谓的 False Sharing。
现代 CPU 不会按“一个变量”来缓存内存,而是按一整块来搬运,常见大小是 64 字节,这一块就叫 cache line。假设两个线程分别修改
a 和 b,如果这两个变量刚好落在同一个 cache line 里,那么线程 1 一写 a,CPU 就会把这整行标记成自己最新;线程 2 再写 b,又会把同一行抢过去。于是两个核心来回“打架”,虽然改的不是同一个变量,但底层维护缓存一致性时,看到的是同一块内存,所以会反复失效、同步、重取,最后浪费大量时间在 cache coherence 上,而不是算业务逻辑。这就是它名字里“false”的意思:表面上看像共享写入冲突,实际上程序语义上根本没有在争同一份数据。设计上,CPU 必须按 cache line 管理,因为按字节追踪一致性成本太高,硬件根本扛不住。所以这不是 CPU “笨”,而是工程上的现实取舍:用较粗粒度的缓存块换取整体吞吐,但代价就是程序布局不当时会踩坑。
最常见的坑出现在计数器、统计数组、任务队列元数据里。比如你开 8 个线程,每个线程维护自己的计数,本来以为不会互相影响,于是写成一个连续数组
counters[8]。结果这 8 个整数很可能挤在一两个 cache line 里,线程越多,互相打得越狠。解决办法不是“加锁”,那只会更糟;真正的修复方式是让高频写入的数据在物理布局上分开,比如 padding、按 cache line 对齐,或者干脆让每个线程持有自己独立的局部状态,最后再汇总。但也别走到另一个极端。不是所有相邻变量都该强行 padding 成 64 字节,那会浪费内存,还可能让 cache locality 变差。关键判断标准很简单:如果多个线程会高频写,并且这些数据可能挨在一起,就该警觉;如果只是只读共享,或者很少修改,False Sharing 往往不是主因。
🧪 课后一题:有 4 个线程分别不停执行
cnt[i]++,cnt 是一个连续的 int cnt[4]。逻辑上每个线程只改自己的下标,但程序吞吐量很差。最可能的原因是什么?int💡 易混淆点:False Sharing 不是“多个线程读写同一个变量”,而是“多个线程写不同变量,但这些变量落在同一个 cache line 里”。
#CS