隐藏性能杀手之 '伪共享'

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

CPU的缓存结构以及内存硬盘:

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

存储器 存储介质 介质成本(美元) 随机访问延迟
L1 cache SRAM 7 1ns
L2 cache SRAM 7 4ns
Memory DRAM 0.015 100ns
Disk SSD(NAND) 0.0004 150us
Disk HHD 0.00004 10ms

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

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

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

$ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_ltne_size

一般主流的CPU都将会是64字节的 CPU Line,但苹果研发的M1CPU,其 CPU Line已经是128字节。

我们知道了内存中的数据是以一块CPU Line 缓存行进行拷贝到CPU Cache中的,那么接下来,我们就来介绍一下 Cache 伪共享是什么?又如何避免这个问题?

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

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

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

为了准守 MESI,多核心直接不得不通知其他核心,他持有的 CPU Line 已经过期被修改过,需要重新去内存中获取一遍,从而导致性能有损耗。

当多个CPU核心同时访问同一个Cache Line时,就会出现伪共享问题。这个问题会导致性能下降,因为当一个CPU核心更新了Cache Line中的数据时,其他CPU核心需要重新从内存中读取这个数据,而这个过程需要耗费一定的时间。

为了避免伪共享问题,可以使用Cache Line填充技术。这个技术的原理是在Cache Line中添加一些无用的数据,使得不同的CPU核心访问同一个Cache Line时,它们访问的数据不会重叠,从而避免了伪共享问题。

此外,还可以使用一些编程技巧来避免伪共享问题。比如,可以将需要访问的数据放到不同的Cache Line中,或者使用线程本地存储(Thread Local Storage,TLS)来避免不同线程之间的数据竞争。除了Cache Line填充技术,还有其他的一些技术可以用来避免伪共享问题。比如,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相关的性能问题,从而保证系统的性能和稳定性。