What Every Programmer Should Know About Memory -CPU Caches 译文

1,357 阅读9分钟
今天尝试从硬件Cache应用来分析缓存,得到缓存应用需要面对的问题,以及精妙的解决方案。
分析过程中发现首先对硬件的缓存就不甚了解,查阅的中文资料也不能满足需求。
但是从中文资料中知道了《What Every Programmer Should Know About Memory》这篇论文。
决定阅读原文《CPU Caches》章节,并将阅读中的翻译结果记录在此。

如今的CPU已经比25年前精密很多。在那段时间,CPU内核频率与内存总线频率相当。内存的访问比寄存器的访问只是慢一点。但是这个状况在20世纪90年代被打破,CPU设计师增加了CPU内核频率,而内存总线频率与RAM性能并没有相匹配的提高。并不是因为上一节中讲的高速RAM造不出来。可以造出来,但是成本高昂。跟当前CPU内核一样快的RAM,比任何DRAM都要贵几个数量级。

假定有两种计算机,第一种RAM容量很小、速度很快,第二种RAM容量很大,速度相对快。在工作集大小超过容量较小的RAM,辅存访问成本类似硬盘的情况下,在这两种计算机做一个选择。那第二种计算机总是会被选择。原因在于,用来容纳工作集换出部分的辅存(通常是硬盘),其访问速度比DRAM都要慢几个数量级。

幸运的是,这不是一个只能二选一的决策。计算机可以在拥有大存储量DRAM的同时,配备小存储量的SRAM。一种可能的实现是,把处理器的地址空间分一部分专门来放SRAM,剩余的再放DRAM。这样的话,操作系统的任务将是保证数据的最优分配,以充分利用SRAM。这个方案下,SRAM基本上就是寄存器组的一个延伸。

尽管这是一个可能的实现,但是不可行。抛开SRAM物理存储映射到进程虚拟地址空间的问题(映射本身就很难)。这方法要求每个软件进程来管理自己的内存区域分配。内存区域的大小可以随处理器的不同而不同(不同的处理器可以有不同大小的SRAM存储)。每个程序模块都会声明对高速存储的使用,从而产生同步需求,增加额外成本。总之,拥有高速存储的好处,会被管理资源的开销完全抵销。

因此,让SRAM对操作系统及用户透明,不受控于操作系统或用户,而由处理器管理。在这种模式下,SRAM 用来临时备份(或者说缓存)主存中可能被处理器马上使用的数据。这是有可能做到的,因为程序与数据有时间和空间局部性。局部性意味着在短时间内,可能会重复使用相同的代码或者数据。对代码来说,这意味着很有可能会有循环,所以相同代码会被一遍一遍的重复使用(空间局部性的完美案例)。数据获取也被理想的限制在一个小区域内。即使短时间内使用的内存不是连续的,但是相同的数据也很可能在不久后被再次使用(时间局部性)。代码方面举例来说,循环内调用一个函数,函数可能放在地址空间其它地方,可能离循环代码很远,但是这个函数会在短时间内被多次调用。在数据方面,意味着一次使用的内存总量(工作集大小)有限,另外因为RAM的随机访问性,使用的内存不连续。要意识到,局部性是我们如今使用的CPU缓存概念的关键。

一个简单的计算可以表明缓存在理论上如何起作用。假设主存访问需要200个时钟周期,缓存需要15个时钟周期,代码中对100个数据使用100次。如果没有一次缓存命中,直接访问主存,需要花费2,000,000个时钟周期。而如果全部命中,只需要花费168,500个时钟周期。这减少了91.5%的耗时。

用来做缓存的SRAM通常比主存小。以作者使用带CPU缓存的个人工作台经验来看,缓存大小一般是主存的1/1000(现在是4MB缓存,4GB主存),这并不成问题。如果工作集(当前处理的数据集)大小小于缓存大小,则更没有关系。但是计算机不会无缘无故的有个大主存,工作集必然是大于缓存的。特别是运行多进程的系统,工作集大小为所有进程与内核总和,更是大于缓存。

在缓存大小受限的情况下,需要优秀的解决方案,在任意时间都能决策哪些东西应该被缓存。因为不是所有的工作集数据都会同时使用,所以我们可以使用技术来替换缓存数据。也许替换可以在数据被真正使用前完成。预读取可以减少一些访问主存的开销,因为这对于程序来说是异步的。所有这些技术以及更多可以使用的技术用来让缓存作用比它实际容量大很多。我们会在第3.3节中讨论它们。在第六章中将会讨论如何利用这些技术来帮助程序员使用处理器。

3.1 CPU缓存的位置

在投入到CPU缓存实现细节之前,一些读者可能发现,首先看看缓存是如何配合现代计算机系统,会更有益于理解CPU缓存。

图3.1简化展示了拥有CPU缓存的早期系统的缓存结构。CPU内核不再直接连接主存。所有的读写操作都必须通过缓存。CPU内核与缓存之间的是专门的快速连接。在简化示意里,主存跟缓存通过系统总线连接,这个系统总线也会用作其它部件与系统的通讯。我们现在引入了FSB的概念来代替系统总线概念,具体参见2.2节。在本节内容中,我们会忽略可能存在的,用来促进CPU与主存通信的北桥。

尽管过去几十年里绝大多数计算机使用的冯·诺依曼结构,但是经验表明,把缓存分为代码缓存与数据缓存是有好处的。英特尔自1993年以来,一直将代码缓存与数据缓存分开使用,并不再回头合并。代码与数据需要的内存区域之间彼此独立,所以独立缓存更加有效。在最近几年又显现了其它优势:大多数处理器解码指令是比较慢的;缓存解码指令可以提高执行速度,在因为错误的预测,或者不可能预测分支情况下导致的流水线空置情况下,对执行速度的提升特别明显。

指令缓存是直接能解码更快吗?还是说指令缓存可以异步解码,让CPU做别的事情?
希望清楚原理的能指导下。

引入缓存不久后,系统就变得更加复杂。缓存与主存的速度差异继续增加,到一个点后,又增加了另外一级缓存,这级缓存比一级缓存更大、更慢。因为经济原因,只增加一级缓存大小不可行。现在,甚至有三级缓存的机器也常态化了。三级缓存处理器如图3.2。随着单个CPU中的核数增加,未来缓存的级次可能也会更多。

图3.2展示了缓存的三级结构,和后面文档部分我们要使用的术语。L1d是1级数据缓存,L1i是一级指令缓存,等等。注意,这是示意图,从内核到主存真实的数据流是不需要通过任何高级次缓存的。CPU设计师有很高的自由度来设计缓存接口。对程序员来说,这些设计是透明的。

此外,多核处理器的每个核都可以有多“线程”。核与线程的区别在于,不同的核各自有独立的硬件资源,除非使用相同资源(比如,同时获取外部连接),否则核可以完全独立运行。另一方面,不同的线程几乎会共享处理器的所有资源。英特尔的线程实现仅仅是给线程分配不同的寄存器,甚至这个不绝对,有的寄存器是共享的。完整的现代CPU外观如图3.3。

图中我们有两个处理器,每个处理器有两个核,每个核有两个线程。线程共享一级缓存。CPU的所有核共享高级次缓存。两个处理器(浅灰色的两个大框)不会共享任何缓存。这些信息很重要,尤其是在我们讨论缓存对多进程与多线程应用的影响的时候。

3.2 高级缓存操作

为了搞懂使用缓存带来的开销和节省的成本,我们必须将第2章中机器结构与RAM 技术的知识,与前一节中的缓存结构的介绍结合起来。

默认情况下,CPU内核读写的所有数据都存储在缓存里。有些内存区域无法被缓存的,但这个只需要操作系统实现考虑,对应用程序的程序员是透明的。还有指令允许程序员故意绕开缓存,这个将在第6章讨论。

未完待续...