Poison

False Sharing

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

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


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

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


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


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

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

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


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


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

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

ConcurrentHashMap 中也用到了 @Contended 来避免伪共享的问题,以提升计数的效率。源码位于 ConcurrentHashMap.java at jdk8-b120:

1
2
3
4
5
6
7
8
/**
* A padded cell for distributing counts. Adapted from LongAdder
* and Striped64. See their internal docs for explanation.
*/
@sun.misc.Contended static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}

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

Reference

现代操作系统(原书第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