[译] 为什么 CPU 有多层缓存

3,449 阅读11分钟
原文链接: bit.ly

译者介绍:杨川,英文名Jerry,就职于IBM全球服务中心。

这是来自于‘jlforrest’的一位读者的提问,我认为值得更详细的解答。以下是他的问题:

我理解缓存,但不明白为什么需要多层缓存而不是直接用一层大的。换句话说,如果一级缓存是32K,二级256K,三级2M,为什么不用32K+256k+2M的一级缓存?

简短的回答是不同的缓存级别适用于不同的目的和限制,在设计上完全不同。经验上,随着缓存级别的增加,缓存变得更大,更慢,密度更高,每单位存储消耗的电能更少,能处理更多的任务。

为了把这个问题讲得更直观,我打算用一个有点奇怪但很精巧的类比:

缓存的故事

假设你是一位六十年代的白领,在巨大的办公楼里工作,没有电脑,你需要阅读大量的文件并且交叉检索这些文档。

你有一个办公桌(L1 缓存)。桌上的文件是你正在手头处理的资料,还有一些是你最近看过的或者你准备阅读的。通常我们需要阅读文件的每一页(对应于存储单元的一个字节),但除非它们在办公桌上,文件都是作为一个整体。即只想看某一页的内容,我们也必须把整份文件抓过来。

办公室里还有文件柜(L2 缓存)。这些文件柜里存放的是你最近处理过,但目前没有在使用的文件。办公桌上的文件在用完后,通常也会放回文件柜。从文件柜里拿文件就不是顺手拈来了——你需要走过去,打开相应的抽屉,还要查目录卡片,才能找到想要的文件——不过这也还比较快了。

有些时候,其他人也需要查看你的文件柜里的文件。勤杂工会推着一辆推车(环路公共汽车)在各个办公室转。如果有人在自己的文件柜没有找到相应的资料,他会写一个纸条交给勤杂工。为简化起见,假设这位勤杂工知道所有的东西放哪儿。所以当他来到你的办公室的时候,他会检查你的文件柜里是否有其他人需要的文件,如果有,就把这些文件抽出来放到车上。当他转到别的办公室,就会把找到的文件放在文件柜里,并留下收条。

有时候,这些文件并不在文件柜里,而是在办公桌上。那就不可以直接拿了,需要征询主人的意见,如果不行,大家就要商量如何协调。有大量冗长的详尽的合作指引来处理这类情况(至少要一起开会)。

文件柜经常会满,这时就不能放新的文件,需要先腾地方,把一些很久都没用到的文件拿出来。勤杂工会把这些文件放到地下室里(L3 缓存)。地下室里的文件被密集地堆放到纸箱里或者文件架上,交给文档管理员处理,其他人都不会下去,也不会了解文档的存放细节。

当勤杂工来到地下室,会把一堆不需要的文件放到‘in’框里,同时他也会留下一堆纸条,写着在楼上文件柜里找不到的文件名。文档管理员会拿着这些纸条,找到对应的文件,把它们放到‘out’ 框里。下次勤杂工下来的时候,就可以把‘out’框里的文件拿走,交给需要的人。

现在的问题是,文件还是太多,地下室也放不下,而且办公大楼的租金都很贵,所以通常公司都会在离市区较远的地方租一个仓库来存放归档文件(对应于DRAM内存)。文档管理员会记录哪些文件放在地下室,哪些文件放在仓库。这样,当需要拿文件时,管理员就知道哪些是能在地下室找到,哪些要到仓库里拿。每天有一两次,会有一辆货车开到仓库去拿需要的文件,同时把一些地下室的旧文件运过去。

对勤杂工而言,他并不关心这些细节,这些都是文档管理员在处理。他需要做的就是把纸条放到‘in’框里,从‘out’框里取出文件。

回到最初的问题

那么,这个类比的意义何在?简短而言,一个具体的模型比模糊的概念更能清晰地阐明物流的意义。实际上,物流对设计芯片的意义和运作一个高效的办公文件处理系统是一样的。

最初的问题是‘为什么不用一个大的缓存,而是用几层小的缓存?’。 也就是说,如果一个4核芯片配置32K一级缓存,256K二级缓存和2M三级缓存,为什么不能用一个3M的大缓存?

在类比里,类似于问与其给4个人每人分一个1.5米宽的办公桌,为什么不给这4个人一个150米宽的大办公桌?

关键在于,放办公桌上的目的就是要能触手可及。如果办公桌太大,就没有意义了。难道还需要走50米去拿文件? 对一级缓存也是同理,如果太大,存取速度会变慢,而且会消耗更多的电力。所以一级缓存既要足够大到能起作用,又要小到能够快速存取。

另外一点,一级缓存处理的存取类型和其他缓存不同。有L1的数据缓存,也有L1指令缓存。Intel的CPU还有另外的指令缓存,uOp缓存,既叫L1并发指令缓存也叫L0指令缓存。

L1数据缓存通常只处理1到8个字节的数据,但高层级的缓存并不处理单独的字节。在我们的类比里,所有不办公桌上的资料都是以文件为单位(对应于catch line)。 在内存中也一样,高层级缓存通常是批发处理数据,以缓存行为单位(cache line)。

L1指令缓存和数据缓存完全不同,就内核而言,它是只读的。(指令内存的写入通常是用非直接的方式,先把指令放入高层的缓存,再载入一级指令缓存)。由于这个原因,指令缓存和数据缓存通常是分隔的。使用通用的L1缓存意味着把互相冲突的设计原则糅合在一起,妥协的结果就是任何一个目的都达不到。而且用通用的L1缓存处理指令和数据负载也会很大。

另外,作为程序员,我们通常不关心内存带宽。例如,每个时钟周期,i7的CPU的内核能从L1缓存中读取16字节的指令,而且会不断地循环读取。如果是3GHZ,每个核可以读50GB指令/秒。实际上,通常L1指令缓存的能力都足够大,很少需要L2缓存参与处理。但如果是通用缓存,就需要预估指令和数据的高并发情况。(想象一下在L1缓存中用memcopy拷贝几K数据的情况)

顺便提一句,如果都在L1缓存,CPU能在一个时钟周期完成许多存取操作。‘Haswell’或者之后的3GHZ的i7内核可以处理超过300GB的指令和数据, 如果搭配合理的话。这样的处理能力绰绰有余,但你仍然需要考虑数据和指令同时出现峰值的情况。

L1缓存在设计上就是越快越好,以应对峰值情况。只有L1缓存处理不了,才会转给更高层的缓存,但速度会降低。因为高层缓存更关心电力效率和存储密度。

第三点,共享。在上面的比喻中,独立办公桌,亦或L1缓存是私有的,如果在你的办公桌上,你只管拿就好了,不需要询问其他人。

这很关键。如果4个人共享一个大办公桌,你就不能随便拿文件,因为另外三个人可能正在使用。(即使他们只是在阅读其他文件时顺便参考一下你想使用的文件)。任何时候,你想要拿什么东西,你需要先叫一声,‘有人在用吗?’如果别人在你前面,你就必须等待。或者需要一个排队系统,当存在资源冲突的时候,每个人需要拿张票排队等候,或者其它的什么机制,具体实现细节并不重要,但是所有的事情你都需要和其他人协调。

对多核共享缓存的情况也是这样。你不能在不通知别人的情况下随意动那些数据,所有对共享缓存的操作都必须协调进行。

这就是为什么我们要使用私有的L1缓存。L1缓存就是你的办公桌,你可以随便使用桌上的文件。L2缓存处理大部分的协同操作。大部分时间,工作者(CPU内核)坐在办公桌前,勤杂工会走过来,把需求列表拿走,同时把之前你想找的文件放倒文件柜里。整个过程不会打断你的工作(CPU)。

仅仅当你和勤杂工都需要拿文件柜里的同一份文件,或者别人想用你办公桌上的文件,这时就需要停下手头工作,进行交谈。

简单而言,L1缓存的工作就是优先为CPU内核服务。因为是私有的,所以基本不需要协调工作。L2缓存也是私有的,但是它的工作重心还包括在不打扰内核工作的情况下,处理大量的缓存间的数据通信。

L3缓存是共享资源,需要全局协调。在上面的类比中,工作者只有从勤杂工的推车里拿到文件,这就是一个阻塞点。我们只能希望L1和L2缓存足够大以便这类阻塞点不会成为性能瓶颈。

附加说明

本篇文章涵盖了当前台式机(笔记本)CPU的缓存架构:分开的L1/L1 D 缓存,每核统一的L2缓存,共享的L3缓存。

不是每个系统都象这样。一些系统并不区分指令缓存和数据缓存;另外一些则把指令和数据在所有的缓存级全部分开。很多L2缓存是多核共享的,L2缓存就象是连接多个内核的公共汽车。还有一些系统有L3和L4缓存。我也没有提到使用多CPU套接字的系统。

我提到环路公共汽车是因为这是一个很好的类比。环路公共汽车很常见。有些时候,环路汽车是个麻烦(尤其是只需要把两三个街区连起来);有时候,环路公共汽车需要和交叉系统连接起来(就象在办公室,每个楼层用推车,不同楼层则用电梯)。

作为软件工程师,我们自然而然地会假设模块A和模块B可以魔术般地连接,数据则可随意地从一端流向另一端。内存的实际工作机制其实非常复杂,但抽象出来呈现给程序员的只是一组大平面的字节排列。

硬件并不象这样工作。各个部件之间并不能魔法般地自动连接。模块A和模块B并非抽象概念,而是具体的物理设备,实际上可以看作是非常小的机器,在硅片上占有实际的物理空间。芯片都有平面图,是真正的2D地图。如果你想连接A和B,就需要一条实际的导线来连接。 导线也占空间,而且需要消耗电力(越远消耗越多)。用一大捆线连接A和B意味着物理上这一块区域会阻碍其他区域的连接。(当然,芯片可以使用多层连接,如果你有兴趣,可以搜索‘routing congestion’)。 在芯片里移动数据实际上是一个物流问题,并且超级复杂。

所以尽管办公室的故事只是半开玩笑的类比,‘谁需要和谁谈’、‘这个系统的几何构造如何——是有意义的布局吗?’这些问题其实对系统设计和硬件相关并有巨大的影响。 利用空间的隐喻来概念化实际情况是十分有效的。

对『Hacker时间』感兴趣的同学可以扫码进群一起来讨论。