缓存行如何工作?


166

我了解处理器会通过高速缓存行将数据带入高速缓存,例如,在我的Atom处理器上,无论所读取的实际数据大小如何,一次都会引入约64个字节。

我的问题是:

想象一下,您需要从内存中读取一个字节,这64个字节将被带入缓存?

我看到的两种可能性是,这64个字节从感兴趣的字节下方最接近的64个字节边界处开始,或者这64个字节以某种预定的方式散布在该字节周围(例如,一半以下,一半以上或上面所有)。

哪有


22
阅读:每个程序员都应该了解内存。然后再读一遍。更好的(pdf)源在这里
andersoj

Answers:


128

如果包含正在加载的字节或单词的高速缓存行尚未存在于高速缓存中,则您的CPU将请求从高速缓存行边界开始的64个字节(所需最大地址以下的最大地址为64的倍数) 。

现代PC内存模块一次传输64位(8字节),一次传输八次,因此一个命令触发从内存中读取或写入整个高速缓存行。(DDR1 / 2/3/4 SDRAM突发传输大小最多可配置为64B; CPU将选择突发传输大小以匹配其缓存行大小,但是64B是常见的)

根据经验,如果处理器无法预测内存访问(并预取),则检索过程可能需要约90纳秒或约250个时钟周期(从CPU知道地址到接收数据的CPU)。

相比之下,在现代x86 CPU上,L1高速缓存的命中具有3或4个周期的加载使用延迟,而存储重载具有4或5个周期的存储转发延迟。在其他体系结构上情况相似。

进一步阅读:乌尔里希·德雷珀(Ulrich Drepper) 《每个程序员应该了解的内存》。软件预取建议有些过时:现代的硬件预取器更智能,并且超线程比P4时代要好(因此预取线程通常是浪费的)。另外, 标签Wiki具有该体系结构的许多性能链接。


1
这个答案完全没有道理。64位内存带宽(在这方面也是错误的)与64位字节(!)的不相关关系是什么?如果撞到Ram,10到30 ns也是完全错误的。对于L3或L2高速缓存可能是正确的,但对于更像90ns的RAM可能不是。您的意思是突发时间-以突发模式访问下一个四字的时间(实际上是正确的答案)
Martin Kersten

5
@MartinKersten:DDR1 / 2/3/4 SDRAM的一个通道确实使用64位数据总线宽度。整个高速缓存行的突发传输确实需要八次传输,每次传输8B,这是实际发生的情况。通过首先传输包含所需字节的8B对齐块,即从那里开始突发(如果不是突发传输大小的第一个8B,则环绕),来优化该过程仍然是正确的。但是,具有多级缓存的现代CPU可能不再这样做了,因为这意味着将突发的第一个块提前中继到L1缓存。
彼得·科德斯

2
Haswell在L2和L1D缓存之间具有64B路径(即完整的缓存线宽),因此,传输包含请求字节的8B将导致该总线的低效使用。@Martin关于必须访问主存储器的负载的访问时间也是正确的。
彼得·科德斯

3
关于数据是否一次全部到达内存层次结构,还是L3在开始将其发送到L2之前是否等待内存中的完整行,这是一个很好的问题。在不同级别的缓存之间有传输缓冲区,每个未完成的未命中都声明一个。因此,(总的猜测)大概是L3将字节从存储控制器放到它自己的接收缓冲区中,同时又将它们放到了想要它的L2高速缓存中合适的加载缓冲区中。当该行从内存中完全传输完后,L3通知L2该行已准备就绪,并将其复制到其自己的阵列中。
彼得·科德斯

2
@马丁:我决定继续编辑此答案。我认为它现在更准确,而且仍然很简单。未来的读者:另请参阅Mike76的问题和我的答案:stackoverflow.com/questions/39182060/…–
彼得·科德斯

22

如果高速缓存行为64字节宽,则它们对应于以64整除的地址开始的内存块。任何地址的最低6位都是高速缓存行的偏移量。

因此,对于任何给定的字节,可以通过清除地址的最低有效六位来找到必须提取的高速缓存行,这对应于舍入到最接近的可被64整除的地址。

尽管这是通过硬件完成的,但我们可以使用一些参考C宏定义来显示计算:

#define CACHE_BLOCK_BITS 6
#define CACHE_BLOCK_SIZE (1U << CACHE_BLOCK_BITS)  /* 64 */
#define CACHE_BLOCK_MASK (CACHE_BLOCK_SIZE - 1)    /* 63, 0x3F */

/* Which byte offset in its cache block does this address reference? */
#define CACHE_BLOCK_OFFSET(ADDR) ((ADDR) & CACHE_BLOCK_MASK)

/* Address of 64 byte block brought into the cache when ADDR accessed */
#define CACHE_BLOCK_ALIGNED_ADDR(ADDR) ((ADDR) & ~CACHE_BLOCK_MASK)

1
我很难理解这一点。我知道2年后,但是您能给我示例代码吗?一两行。
尼克

1
@Nick此方法起作用的原因在于二进制数字系统。2的任何幂只设置了一位,所有剩余的位都被清除了,因此对于64,您已经0b1000000注意到,最后6位是零,所以即使您有一些数字与这6个集合中的任何一个(代表数字, %64),清除它们将为您提供最接近的64字节对齐内存地址。
legends2k

21

首先,主存储器访问非常昂贵。当前,一个2GHz的CPU(最慢的一次)每秒具有2G的滴答声(周期)。一个CPU(现今的虚拟内核)可以在每个周期内从其寄存器中获取一个值。由于虚拟内核由多个处理单元(ALU-算术逻辑单元,FPU等)组成,因此,如果可能,它实际上可以并行处理某些指令。

主存储器的访问成本约为70ns至100ns(DDR4稍快一些)。这次基本上是在查找L1,L2和L3高速缓存,然后击中内存(将命令发送到内存控制器,然后将其发送到内存库),等待响应并完成。

100ns表示约200个滴答声。因此,基本上,如果程序始终会丢失每个内存访问的缓存,则CPU将花费其99.5%的时间(如果仅读取内存)空闲等待内存。

为了加快速度,这里有L1,L2,L3缓存。他们使用直接放置在芯片上的存储器,并使用不同类型的晶体管电路来存储给定的位。因为通常使用更先进的技术来生产CPU,并且L1,L2,L3存储器的生产故障有可能使CPU一文不值(缺陷),所以这比主存储器占用更多的空间,更多的能量并且成本更高。大的L1,L2,L3高速缓存会增加错误率,从而降低良率,从而直接降低ROI。因此,在可用缓存大小方面要进行巨大的权衡。

(当前,人们创建了更多的L1,L2,L3高速缓存,以便能够停用某些部分,以减少实际生产缺陷是高速缓存存储区整体上导致CPU缺陷的机会)。

给出定时想法(来源:访问缓存和内存的成本

  • L1快取:1ns至2ns(2-4个周期)
  • L2快取:3ns至5ns(6-10个周期)
  • L3快取:12ns至20ns(24-40个周期)
  • RAM:60ns(120个周期)

由于我们混合使用不同的CPU类型,这些只是估计值,但是可以很好地了解在获取内存值时的实际情况,并且在某些高速缓存层中可能会命中或未命中。

因此,缓存基本上可以大大提高内存访问速度(60ns与1ns)。

取值,将其存储在缓存中以便重新读取它对于经常访问的变量来说是好的,但是对于内存复制操作,它仍然会变慢,因为一个人只能读取一个值,将其写入某个地方而从不读取该值再次...没有缓存命中,非常缓慢(此外,由于我们的执行顺序混乱,因此可以并行发生)。

此内存副本非常重要,以至于有不同的方法可以加快它的速度。在早期,内存通常能够在CPU外部复制内存。它由内存控制器直接处理,因此内存复制操作不会污染缓存。

但是,除了普通的内存副本外,其他串行内存访问也很常见。一个示例是分析一系列信息。拥有整数数组并计算总和,均值,平均值或更简单的方法是找到某个值(过滤器/搜索)是每次在任何通用CPU上每次运行时都非常重要的一类算法。

因此,通过分析内存访问模式,很明显,数据被频繁地顺序读取。如果程序读取索引i处的值的可能性很高,那么该程序也将读取i + 1值。此概率略高于同一程序还将读取值i + 2的概率,依此类推。

因此,给定一个内存地址,预先读取并获取其他值是(现在仍然是)一个好主意。这就是为什么存在升压模式的原因。

提升模式下的内存访问意味着发送一个地址并顺序发送多个值。每个附加值发送仅花费大约10ns(甚至更低)。

另一个问题是地址。发送地址需要时间。为了寻址存储器的大部分,必须发送大地址。在早期,这意味着地址总线的大小不足以在单个周期(刻度)内发送地址,并且需要多个周期来发送地址,从而增加了更多延迟。

例如,一个64字节的高速缓存行意味着将内存划分为大小为64字节的不同(非重叠)内存块。64字节表示每个块的起始地址具有最低的六个地址位,始终为零。因此,对于任何数量的地址总线宽度,都不需要每次发送这六个零位,从而将地址空间增加64倍(欢迎效果)。

高速缓存行解决的另一个问题(除了预先读取并保存/释放地址总线上的六位以外)是高速缓存的组织方式。例如,如果将高速缓存划分为8个字节(64位)的块(单元),则需要存储该高速缓存单元与之一起保存值的存储单元的地址。如果该地址也为64位,则意味着该地址消耗了一半的高速缓存大小,导致开销为100%。

由于高速缓存行为64字节,而CPU可能使用64位-6位= 58位(无需太正确地存储零位),这意味着我们可以以58位(开销为11%)的开销来缓存64字节或512位。实际上,存储的地址甚至比这个要小,但是有状态信息(例如,高速缓存行有效且准确,肮脏并且需要写回ram等)。

另一个方面是,我们具有集合关联缓存。并非每个缓存单元都能够存储某个地址,而只能存储其中的一部分。这使得必要的存储地址位更小,允许并行访问缓存(每个子集可以访问一次,但独立于其他子集)。

尤其是要同步不同虚拟内核之间的缓存/内存访问,每个虚拟内核具有独立的多个处理单元,最后在一个主板上具有多个处理器(这些主板可容纳多达48个处理器,甚至更多)。

这基本上就是我们拥有高速缓存行的当前想法。预先读取的好处非常高,最糟糕的情况是从高速缓存行中读取单个字节,然后再也不读取其余字节,因为这种可能性非常小。

高速缓存行的大小(64)是较大的高速缓存行之间的明智选择,这使得它的最后一个字节不太可能在不久的将来读取,即获取完整高速缓存行所需的时间从内存(并将其写回),以及缓存组织中的开销以及缓存和内存访问的并行化。


1
集关联缓存使用一些地址位来选择一个集,因此标记可以比您的示例更短。当然,高速缓存还需要跟踪哪个标记与集合中的哪个数据阵列一起使用,但是集合中的集合通常比集合中的方式多。(例如,在Intel x86 CPU中具有64B行的32kB 8路关联L1D高速缓存:偏移6位,索引6位。标签仅需要48-12位宽,因为x86-64(目前)仅具有48-位物理地址。正如我确定您知道的那样,低12位是页面偏移量并不是巧合,因此L1可以是VIPT而无需混叠。)
Peter Cordes

惊人的答案芽...哪里有“赞”按钮?
埃德加德·利马

@EdgardLima,不是upvote按钮?
Pacerier

6

处理器可能具有多层缓存(L1,L2,L3),并且它们的大小和速度有所不同。

但是,要了解每个缓存中到底包含什么内容,您必须研究该特定处理器使用的分支预测器,以及程序的指令/数据如何针对它进行操作。

阅读有关分支预测器CPU缓存替换策略的信息

这不是一件容易的事。如果最终您只想进行性能测试,则可以使用Cachegrind之类的工具。但是,由于这是模拟,因此其结果可能会有所不同。


4

我不能肯定地说,因为每个硬件都不同,但是通常这是“ 64字节从下面最接近的64字节边界开始”,因为这对于CPU来说是非常快速和简单的操作。


2
可以肯定地说。任何合理的高速缓存设计都将具有大小为2的幂且自然对齐的行。(例如,与64B对齐)。 它不仅快速简单,而且实际上是免费的:例如,您只需要忽略地址的低6位即可。 缓存通常使用不同的地址范围执行不同的操作。(例如,高速缓存关心标记和索引以检测命中与未命中,然后仅使用高速缓存行内的偏移量来插入/提取数据)
Peter Cordes,2016年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.