Poison

False Sharing

处理器使用不同级别的缓存,当处理器从主内存读取值时,可能会将该值缓存以提高性能。实际上,大多数现代处理器不仅仅缓存请求的值,而是会将该值附近的数据同时进行缓存。这种优化基于空间局部性的思想,可以显著提高应用程序的整体性能。简单来说,处理器缓存工作在缓存行上,而不是对单个请求的值进行缓存。

我们可以看一个例子,假设有两个 CPU 核心 core A 及 core B 将要对内存临近位置上的两个变量进行访问,如下图:

false-sharing-exclusive

core A 从主内存读取变量 a 并将变量 a 在主内存中临近的变量 b 同时获取并缓存到 core A 的缓存行上,然后它将该缓存行标记为 exclusive,因为 core A 是在该缓存行上运行的唯一核心。从现在开始,如果可能,core A 将通过从缓存行读取来避免低效的内存访问。

在以上操作之后,core B 决定从主内存中读取变量 b 的值:

false-sharing-shared

因为变量 a 与变量 b 临近且处于同一缓存行中,所以现在 core A 及 core B 同时将它们的缓存行标记为 shared,现在,假设 core A 决定改变变量 a 的值:

false-sharing-invalid

core A 仅将此更改存储在其存储缓冲区中,并将其缓存行标记为 modified。同时,它会将此更改传达给 core B,使 core B 将其缓存行标记为 invalid

以上就是不同的处理器核心保证它们的缓存行彼此一致的原理。

现在,假设 core B 准备去读取变量 b 的值,因为变量 b 的值没有变化,我们可能希望从 core B 的缓存行中读取,然而,因为 core B 上的缓存行已经被标记为 invalid,所以此时 core B 只能从主内存中重新读取变量 b:

false-sharing-flush

如上图所示,core B 从主内存中读取变量 b 不是影响效率的唯一原因,此次主内存访问将强制将 core A 的缓存行上的数据刷新至主内存,刷新完成后,core B 才能从主内存中读取变量 b 的值,最后,两个核心上的缓存行状态再次变为 shared

false-sharing-shared-again

因此,即使两个 CPU 核心没有操作内存上的同一位置,这也会触发缓存未命中及缓存行刷新,这种现象被称为伪共享,可能影响应用程序的性能,尤其当缓存未命中率很高时。更具体地说,处理器会不停访问主内存而不是使用它们的本地缓存。

在 Java 中,提供了 @Contended 注解来处理该问题,该注解会保证变量不出于同一个缓存行上,具体实现机制可以参考下方的文档,java.util.concurrent.atomic.LongAdder 的父类 java.util.concurrent.atomic.Striped64 使用了 @Contended 来处理伪共享问题,以提升计数的效率。

在《现代操作系统》第四版 8.2.5 节分布式共享存储器中对伪共享也有描述。

References

现代操作系统(原书第4版) - 图书 - 豆瓣
A Guide to False Sharing and @Contended | Baeldung
JEP 142: Reduce Cache Contention on Specified Fields
RFR (S): JEP-142: Reduce Cache Contention on Specified Fields