以上表格为 CPU 访问各级缓存和内存所需耗时的对比,可见每一级的耗时差异非常明显。因此,就性能优化而言,应尽可能在更低级别的缓存中命中数据。另外,寄存器、L1 缓存和 L2 缓存是 CPU 核心独占的,每个 CPU 核心都拥有自己的寄存器、L1 缓存和 L2 缓存;而 L3 缓存则是同一 CPU 内多个 CPU 核心共享的;内存则是所有 CPU 所有核心共享的。对于 zStorage 来说,每个 CPU 核心上仅绑定运行一个线程,独占该 CPU 核心的资源(包括寄存器、L1 缓存、L2 缓存),不过 L3 缓存和内存则与其他 CPU 核心共享。
因此,为了使多线程能够尽量互不干扰地并发运行,在数据结构设计上需要进行分区,将数据归属于某个线程。例如:PG1~10 由线程1管理,PG11~PG20 由线程2管理,等等。这样,每个线程只会访问属于自己的那部分数据,不会与其他线程产生竞争。相反,若多个线程(或 CPU 核心)之间存在数据共享,则会导致 CPU 核心为保证数据一致性而频繁锁定总线、修改和加载内存等操作,从而造成性能不佳。
CPU缓存未命中
CPU 先从 L1 缓存查找数据,如果没找到,则继续在 L2 缓存查找,如果仍然没找到,则继续在 L3 缓存查找;如果在所有 CPU 缓存中都未命中,最后才会从内存中读取数据。对于像 zStorage 这样的大型分布式存储系统来说,不太可能将程序运行时所需访问的数据全部放在 CPU 缓存中,因此必然会出现缓存未命中的情况。可行性较高的一些优化方法基本都是通过重排指令和数据以减少缓存未命中问题,例如编译器的优化级别、LTO 优化、PGO 优化等。这些优化方式不会改变数据总量的大小,原本需要访问 1GB 数据,即使经过这些优化,从数据量上也不会有明显的降低,1GB 数据仍然无法完全缓存在 CPU 缓存中,这些优化无非是将时空局部性较强的数据放在一起,从而减少缓存未命中问题。
CPU缓存污染
诸如数据压缩、校验这样大量读取内存数据后进行计算的情况,会将大量数据放入 CPU 缓存中,同时导致原本缓存中的大量有效数据被替换出去。实际上,这些数据是一次性计算的,没有必要经过 CPU 缓存;如果经过 CPU 缓存反而会导致有效缓存数据被替换掉,这种情况被称为 CPU 缓存污染。在 zStorage 中存在压缩、校验等计算场景,不过目前 zStorage 尚未对 CPU 缓存污染情况做处理。据研究,可能的优化方法有绕过 CPU 缓存直接操作内存、L3 缓存分区等等。
从 zStorage 的多线程模型来看,由于 L3 缓存是 CPU 核心间共享的,L3 缓存未命中以及污染问题就成为了线程间的竞争因素。不过,目前尚未针对线程间对 L3 缓存容量竞争做出相应的优化。
在多线程、多内存条的环境下,如何规划哪个线程访问哪个内存条呢?默认情况下,BIOS 会开启内存交织模式,一般是同一个 NUMA 节点上的内存条进行交织,类似于硬盘的 RAID0 模式。这样,任何一个线程的内存操作会被条带化到多根内存条上,从而充分发挥多内存通道的性能。
不过也有一些服务器平台对开启内存交织模式有要求,例如要求每个 NUMA 节点上插满 16 根内存条。那么在少于 16 根内存条的情况下,就可能因内存通道流量差异较大而导致性能问题。zStorage 针对这种情况的处理方式是,在 BIOS 中开启多 NUMA 节点模式(例如 8 个 NUMA 节点,主要是 AMD 平台),然后从每个 NUMA 节点上预先分配一些内存,尽量让各个线程访问属于自己 NUMA 节点的内存。