7. Cache

优质
小牛编辑
122浏览
2023-12-01

PPlain和PPro带有片内cache(一级cache)其中8kb 代码cache,8kb 数据cache。 PMMX,PII 和 PIII 则有 16 kb 代码cache 和 16 kb 数据 cache。 一级 cache 里的数据可以在1个时钟周期内读写,cache 未命中时将损失很多时钟周期。 理解 cache 是怎样工作的非常重要,这样才能更有效的使用它。

数据cache 由 256 或 512 行组成,每行 32 字节。 每次你读数据未命中,处理器将从内存读出一整条 cache 行。 cache 行总是在物理地址的 32 字节对齐。 当你从一个可以被 32 整除的地址读出一个字节, 下 31 字节的读写就不会有多余的消耗。 因此在内存中,你可以把相关数据项放在对齐的32字节块里(集中访问)来获得好处。 例如,如果你有一个循环要操作两个数组,你就可以将两个数组穿插成一个结构数组, 让一起使用的数据的物理位置也在一起。

如果数组或者其它数据结构的尺寸是 32 字节的倍数,你最好将其按 32 字节对齐。

cache 是组相联映像的。 这就是说一个cache 行不能随心所欲地指向任意内存地址。 每个 cache 行有一个 7-bit 的组值,它匹配物理地址的 5 到 11 位 ( 0-4 位指定32字节cache行的行内地址)。 PPlain 和 PPro 可以有 2 条 cache 行对应 128 个组值中的一个值(即每组 2 行),因此对任何RAM地址,可能有两条 cache 行指向它。 PMMX,PII 和 PIII 则是 4 条。

其结果是 cache 保存的地址 5-11 位相同(即具有相同组值)的不同数据块的数目不能超过 2 个(PPlain 和 PPro)或 4 个(PMMX, PII 和 PIII)。 你可以用以下方法检测两个地址是否有相同的组值:截掉地址的低 5 位得到可以被 32 整除的“截断地址”(即令 低5位=0)。 如果两个截断地址之差是 4096 (=1000H) 的倍数, 这两个地址就有相同的组值。

让我用下面的一小段代码来说明一下, 这里 ESI 放置了一个可以被 32 整除的地址:

AGAIN: MOV EAX, [ESI]
MOV EBX, [ESI + 13*4096 + 4]
MOV ECX, [ESI + 20*4096 + 28]
DEC EDX
JNZ AGAIN

这3个地址都有相同的组值, 因为截断地址的差都是 4096 的倍数。 这个循环在 PPlain 和 PPro 上将运行的相当慢。 当你读 ECX 的时候, 没有空闲的 cache 行有想要的组值, 因此处理器用最近最少使用算法替换的两个cache 行中的一个, 这就是 EAX 使用的那个。 然后从 [ESI+20*4096] 到 [ESI+20*4096+31] 读出数据来填充该cache行并完成写ECX的操作。 下一次再 读EAX时, 你将发现为EAX保存数据的cache行已经被丢弃了, 所以又要替换最近最少使用的 cache 行, 那就是保存 EBX 数值的那个了,如此颠簸... 这将会产生大量的 cache 失效, 这个循环大概开销 60 个时钟周期。 如果第3行改成:

MOV ECX, [ESI + 20*4096 + 32]

这样我们就会在 32 字节边界上交错, 因此和前两行的组值不同了。 这样为这三个地址分别指定cache行就没有什么问题了。 这个循环仅仅消耗3个时钟周期(除了第一次运行) —— 一个相当大的提高! 如刚才提到的,PMMX, PII 和 PIII 每组有 4 路cache行,因此你可以有4个相同组值的cache行(一些Intel文档错误的说 PII的cache是2路)。

检测你的数据地址是否有相同的组值可能非常困难,尤其是它们分散在不同的段里。 要避免这种问题的最好能做的就是将关键部分使用的所有数据都放在一个不超过cache大小的连续数据块里,或者放在两个不超过cache一半大小连续数据块 (例如一个静态数据块,一个堆栈数据块) 这样你的cache行就一定会高效使用。

如果你的代码的关键部分要操作很大的数据结构或者随机数据地址,你可能会想保存所有常用的变量(计数器,指针,控制变量等) 在一个单独的最大为 4k 的连续块里面, 这样你就有一个完整的空闲cache行集来访问随机数据。 因为你通常总是需要栈空间来为子程序保存参数和返回地址, 最好的做法是复制所有的常用静态数据到堆栈(把它们复制成动态变量),如果它们被改变,就在关键循环外再复制回去。

读一个不在一级缓存里的数据将导致从二级缓存读入整个cache行,这大约要消耗 200ns (在 100MHz 系统上是 20 时钟周期, 或是 200MHz 上的 40 个周期),但是你最先需要的数据将在 50-100 ns后准备好。 如果数据也不在二级缓存,你将会碰到 200-300 ns 的延迟。 如果数据在 DRAM 页边界交错,延迟时间会更长一些。 (4/8 MB ,72引脚内存芯片的 DRAM 页大小是 1Kb,16/32 Mb 的是 2kb)。

当从内存读入大块的数据, 速度限制在于填充 cache 行。 有时你可以以非连续的次序读取数据来提高速度: 在你读完一个cache行之前就开始读下一个cache行的第一个数据。 在PPlain和PMMX上读主存或二级cache,以及在PPro,PII,PII上读二级cache,用这个方法可以提高读入速度20 - 40%。 这个方法的不利地方在于使程序代码变的非常的笨拙和难于理解。 关于这些技巧的更多信息请参考 http://www.intelligentfirm.com/

当你写向一个不在 一级cache 的地址,在 PPlain和PMMX上这个数值将直接写到 二级cache 或者是 RAM(这取决于2级cache如何设置)。 这大约消耗 100 ns。 如果你向同一个32字节的内存块反复写8次或8次以上但没有从里面读,而且这个块不在一级缓存, 那么较好的做法是先对该块作一个“哑读”使其进入cache行,如此一来随后所有向这个块的写操作就会被定向到cache里,每次只消耗一个时钟周期。 在 PPlain 和 PMMX 上,有时会因为重复写向一个地址而在其间没有读它而带来小小的惩罚。

在 PPro,PII 和 PIII上,一次写操作的cache失效通常会导致读入一个cache行,但也有可能使存储区域做不同的操作,例如显存 (见 Pentium Pro 系列开发者手册, vol.3 : 操作系统写作者指南")。

提高内存读写速度的方法在下面的27.8章节讨论。

PPlain 和 PPro 有 2 个写缓存,PMMX, PII 和 PIII 有 4 个。 故在 PMMX, PII 和 PIII 上你最多可以有4个未完成的不命中cache的写操作而不会使后面的指令产生延迟。 每个写缓存可以处理的操作数宽度最多64位。

临时数据可以方便地放在堆栈里,因为堆栈区域非常有可能在cache中。然而,如果你的数据元素大于堆栈字大小时应该注意对齐问题。

如果两个数据结构的生命期不重叠的话,那么它们可能会共享相同的 RAM 区域从而提高cache效率。这与在堆栈中为临时变量分配空间的普遍习惯是一致的。

将临时数据保存在寄存器里将有更高的效率。 既然寄存器是一种稀有资源,你可能想用[ESP]而不是[EBP]来定位堆栈里的数据, 这样就可以释放EBP用于其它用途。 不要忘记了 ESP 在你每次做 PUSH 或者 POP 时都会被改变。 (你不能在 16位 Windows下使用ESP, 因为时钟中断将在你的代码中不可预知的位置修改ESP的高字。)

有一个分开的cache给代码使用, 它和数据cache是类似的。 代码cache 的大小在 PPlain和PPro上是 8 kb, 在 PMMX,PII和PIII上是 16 kb。 能让你的代码的关键部分(最里面的循环)放入代码cache是很重要的。 最常用的代码或者需要一起使用的过程最好是储存在临近的位置。 不常用的分支或者过程应该放远离些,放在代码的下面或者其它的位置。