CPU架构 - CPU的各级缓存

文摘   2024-08-19 08:00   美国  

引言 — 这篇文章来自Litrin,原文发表于其知乎及个人开源小站




对目前主流的x86平台,CPU的缓存(cache)分为L1,L2,L3总共3级。也有部分的文章中有FLC(first-level cache), MLC(mid-level cache), LLC (last-level cache)的方式区分目前的3级缓存。CPU cache通过比内存(Dram)有更低的时延达到了加速数据读取的效果。

作为Linux的用户,可以使用系统默认安装的lscpu命令简单的列出CPU各级缓存的大小,如我用的一台AMD 7742 CPU具有32K的L1i L1d;512K的L2以及可见的16M的L3(这里埋个伏笔)。



I. 各级缓存的用途


首先说说L1i和L1d的区别,i指的是instruction指令缓存,d是数据data缓存。作为冯·诺依曼体系的计算机,x86价格的指令和数据在内存中是统一管理的。但由于两者内容访问特性的不同(指令刷新率更低且不会被复写),L1的缓存是做了区分的。当前的Intel平台中L1缓存的时延为3个时钟周期,以2.0GHz的CPU计算约1.5纳秒。这种级别的时延可以极大的加速超线程以及CPU分支预测带来的性能优势。

L2缓存的时延是L1的5倍左右,即8ns。每个CPU的物理核心都有自己独立的L2缓存空间。而L3的时延在50~70个时钟周期,30ns。不同于L2,L3缓存是多个核心共享的,L3在使用场景中最大的用途是减少数据回写内存的频率,加速多核心之间的数据同步。

说到L3的“多核心之间”共享,传统的设计是每个CPU插槽或者一块硅片共享一个L3空间,由于内存空间地址是唯一的,这就可能牵扯到同一份内存地址存在两份相同的缓存内容。MESI状态控制就是为了同步数据在多个L3空间之间的流转而设置的,有兴趣可以阅读我的另一篇帖子:

https://zhuanlan.zhihu.com/p/54876718 


另:内存和cpu cache的访问统一都是64byte对齐的。也就是说即便你只需要读取1bit的数据,CPU还是会把64byte的数据从内存逐步扔到L1。

此外,CPU中事实上还存在着TLB(Translation Look aside Buffer,页表缓存)的组件类似于cache的功能。它主要负责缓存页表逻辑到物理地址的对应关系。跟L1类似,TLB也分为iTLB和dTLB分别对应了指令页表和数据页表的地址转换结果。

II. 缓存大小的测量方式


自然,你可以像我一样通过lscpu命令获得CPU各级内存的大小。但在这里,我想找一个直观测试方法的例子让大家感受一下不同大小的缓存之间的关系。我使用到了lmbench的一个组件lat_mem_rd这个工具可以创建不同大小的内存对象,通过访问该对象的时延我们可以用来简单的标定各级CPU缓存的大小。

上图展示了我在AMD 7742 CPU上得到的测试数据。横坐标为内存对象的大小,纵坐标为对应的参考时延。很明显的,线图展示了“4级阶梯”的模样。分别对应的CPU命中L1~L3以及命中内存的时延差距。

前面说的一个伏笔:AMD官方给出的数据是“7742CPU的L3缓存大小为256M,而lscpu看到的L3缓存仅为16M”。上图中的“第三到第四级”台阶之间,内存对象的大小恰恰是16M,符合lscpu的结果。这说明AMD Rome架构不同于Intel架构,L3并不是一个整体,它是由16个相互独立的分区组成的,每个分区只能被4个CPU核心共享。——如果你看了上面讲到的L3是“加速多核心之间的数据同步”的,那就是说这种架构下L3的优势被限制在了4个CPU核心之间。

L3 缓存的技术主流又有inclusive和non-inclusive的区别,目前IA两家都逐步采用了后者。两者的区别是inclusive L3实现上L3的内容包含了所有L2的内容;而non-inclusive实现中,L2的内容不会再在L3里出现。显而易见地,non-inclusive更好的节省了宝贵的L3空间。

III. Cache的效能评估


即便是L3的实现成本也远远大于内存。那如何评价Cache是否物尽其用了呢?

对于CPU cache(包含tlb)的效能,可以用MPI(cache misses per instruction,每指令cache不命中率)由于这个值普遍小于1%,有时也会将这个值乘1000计作MPKI(每千次指令不命中数)。考虑到大多数的CPU指令都离不开cache的读写。MPI值越低,说明CPU cache被有效使用的比率越高。

从另一个角度考量,每个应用都有自己特征——这意味着它消耗的缓存大小是不一样的。抽象地说,逐渐增加的cache使用是被cache misses推上来的。如果在cache occupancy和cache MPI之间达到一个平衡其实是非常有讲究的。当然,目前的服务器芯片也逐步增加了对应的L3空间大小划分、管理功能,再次强行推送我之前的笔记帖子:

Litrin:混合部署场景下RDT技术的应用7 赞同 · 1 评论文章

个人的经验是MPI一旦大于4%则认为cache的优化是“不合适的”。但事实上确实存在一种类型的业务(比如流计算)数据不会被反复更新,那额外的cache访问非但没有意义反而会通过LRU挤出效应干扰到共享L3 cache的其他核心上的业务。这个时候就可以考虑使用Non-temporal的读写操作,系统将认为此内存的访问是no-cacheable的。


IV. 两种Cache的区别


inclusive cache和Non-inclusive cache的区别

名称已经很直白了,inclusive/non-inclusive就是数学上的“包含”和“不包含”关系。目前的趋势是逐步从inclusive cache向non-inclusive转变。

如果有了解过LRU,你会发现如果把所有的cache理解为一个整体的话,其实每次的数据读取都会伴随着其他缓存数据的更新。传统上的多层cache考虑到实现难度,严格要求数据保持L3-L2-L1各有一份拷贝。数据update后,一旦被LRU踢出当前缓存则合并更新到下级缓存。L3的大小即为所有缓存数据的最大容量。

另一方面,CPU在不断的演进之后,core的数目越来越多。从前面的内容中你可以知道每个CPU核心都会有独立的L1/L2。那理论上如果继续沿用inclusive L3,L3的容量就必须大大于所有核心上L1/L2的总和才有意义。显然这将会是设计瓶颈。于是就有了non-inclusive的L3——其实non-inclusive的L2很早就有了。

方便你理解的话:non-inclusive cache意味着下级cache事实上是上级cache的回收站。当上级缓存的数据被踢出的时候,踢出的数据回写下级cache。以skylake为例L1没有命中的情况:

  1. L2 miss,跟L3逻辑上同级的SF(snoop filter)记录了各个L2中数据的状态。检查L3和snoop filter记录,获得L3或者其他L2是否有所需数据。

  2. 数据不存在于L3和sf时

    1. 数据从内存直接载入L2。

    2. L1由L2获取数据后将该cache line与当前L1中的旧数据交换空间。

    3. 淘汰的L1数据踢出了L2中的旧数据,数据将从L2写入L3。

    4. L3获得L2淘汰数据并保存,踢出一份旧数据并检查是否需要回写内存。最后更新snoop filter中L2的数据变化。

  3. 数据存在于L3时,像是一个两两交换位置的过程(然而并不是真的两两交换)。

    1. 数据从L3载入L2。

    2. L1由L2获取数据后将该cache line与当前L1中的旧数据交换空间。

    3. 淘汰的L1数据踢出了L2中的旧数据,L2旧数据将从L2回写L3,更新SF。

  4. 数据只存在于sf,意味着当前数据正在被其他核心的L1/L2缓存,有概率内存中是脏数据(已经更新但没有回写内存)CPU将触发MESI流程确保全局一致性(参见:缓存一致性保障一文)。





为感谢支持,已点赞/分享/赞赏10篇/次以上的朋友,请加微信,进入微信群。我将发放免费加入知识星球的链接。

IT奶爸-知识星球



高阅读量文章





IT奶爸
实践是检验“专家”的唯一标准。一群认真执着的IT奶爸的学习和分享。
 最新文章