跳到主要内容

隐藏性能杀手之「伪共享」

· 阅读需 6 分钟

随着CPU工艺的发展,目前的高端CPU已经存在几十核心百多个线程,并为CPU设计出了一二三级缓存。CPU的核心有了这些缓存就可以加快数据的处理,从而减少访问内存的频率,这样CPU的计算性能可以进一步得到提高。

CPU 缓存结构与访问延迟

CPU 的缓存结构以及内存、硬盘的层次关系如下图:

众所周知,CPU 访问一次内存的开销是非常大的,想要获取一次磁盘上的数据更是需要等待较长的时间。虽然目前已经有 mmap 这类技术来缓解这种情况,但总体来说 CPU 的计算性能是整个计算机结构中的天花板,其他硬件在数据传输速度上对比起来就显得拖后腿。我们来看一下 CPU 访问各级存储的延迟:

存储器存储介质介质成本(美元)随机访问延迟
L1 CacheSRAM71ns
L2 CacheSRAM74ns
MemoryDRAM0.015100ns
DiskSSD (NAND)0.0004150us
DiskHDD0.0000410ms

可以看出,外部存储设备容量越大、成本越低,能存的数据更多,但访问速度也更慢;访问速度越快的设备造价越高。CPU 访问 L1 Cache 的速度比访问内存快 100 倍,这就是 CPU 里会有 L1~L3 Cache 的原因——把 Cache 作为 CPU 与内存之间的缓存层,减少对内存的访问频率。

缓存行 Cache Line

CPU 从内存中读取数据到 Cache 的时候,并不是一个字节一个字节读取,而是一块一块地读取,这一块一块的数据被称为 Cache Line(缓存行)。所以 Cache Line 是 CPU 从内存读取数据到 Cache 的单位

至于 Cache Line 的大小,在 Linux 系统可以用下面的命令查看:

# 查看 cpu0 的 L1 缓存行大小,单位为字节
$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size

主流 CPU 的 Cache Line 一般是 64 字节,而苹果研发的 M1 芯片,其 Cache Line 已经是 128 字节。

什么是伪共享

我们知道了内存中的数据是以 Cache Line 为单位拷贝到 CPU Cache 中的,接下来就来介绍 Cache 伪共享是什么,又如何避免这个问题。

假设有一个双核心的 CPU,两个核心并行运行着两个不同的线程,它们同时从内存中读取两个不同的数据:类型为 long 的变量 A 和 B(各 8 字节)。CPU1 想要获取 A 进行修改,CPU2 想要获取 B 进行修改,但这两个数据的地址在物理内存(DRAM)上是连续的。如果 Cache Line 的大小是 64 字节,并且变量 A 在 Cache Line 的开头位置,那么 B 就紧跟在 A 的地址后面,两个数据位于同一个 Cache Line 中。又因为 Cache Line 是 CPU 从内存读取数据到 Cache 的单位,所以这两个数据会被同时读入到两个 CPU 核心各自的 Cache 中。

我们来思考一个问题:如果这两个不同核心的线程分别修改 A、B 的数据,比如 CPU1 的线程只修改了变量 A,CPU2 的线程只修改了变量 B,会发生什么呢?

此时两边核心各自的 Cache Line 中数据就会不一致,这很有可能影响到最终的计算结果。为了保障两边的 Cache Line 数据一致,于是出现了多核缓存一致性的 MESI 协议。

为了遵守 MESI,一个核心修改数据后不得不通知其他核心:你持有的 Cache Line 已经过期,需要重新去内存中获取一遍,这就导致了性能损耗。

当多个 CPU 核心并发修改同一个 Cache Line 中互不相干的变量时,就会出现伪共享(False Sharing)问题。明明各自操作的是不同数据,却因为共享同一个缓存行而互相拖累:一个核心更新了 Cache Line 中的数据,其他核心就要重新加载整行数据,这个过程会耗费不少时间。

如何避免伪共享

最常见的手段是 Cache Line 填充(Padding)。原理是在变量之间填充一些无用的数据,把热点变量隔离到不同的缓存行中,使得不同 CPU 核心访问的数据不再落在同一个 Cache Line 上,从而避免伪共享。

此外还有一些编程层面的技巧,比如把需要频繁修改的数据主动安排到不同的 Cache Line 中,或者使用线程本地存储(Thread Local Storage,TLS)来避免不同线程之间的数据竞争。硬件层面,Intel 提供了 Cache Allocation Technology(CAT),可以让程序员显式地控制 Cache 的分配方式;还有 Cache Partitioning 技术,可以将 Cache 分成多个区域,每个区域分配给不同的线程使用,同样能规避不同线程之间的数据竞争和伪共享问题。

除了伪共享,还有一些其他与 Cache 相关的性能问题需要注意。比如 Cache Miss:当 CPU 需要访问一个不在 Cache 中的数据时,就会产生 Cache Miss,此时 CPU 需要从内存中读取数据,这个过程耗时较长。为了减少 Cache Miss,可以借助预取(Prefetching)、合理的 Cache 替换算法(Cache Replacement Algorithm)等手段。

小结

Cache 是计算机体系结构中提升性能的关键一环,而 Cache Line 作为读写单位,也带来了伪共享这样的隐藏陷阱。多个核心并发修改同一缓存行中的不同变量时,MESI 协议的一致性开销会让性能明显下降。规避思路也很直接:通过填充或数据布局,让热点变量各自独占一个缓存行。在编写高并发代码时,多留意数据在内存中的排布方式,往往能避开这类看不见的性能杀手。

评论 / COMMENTS