caching行如何工作?

据我所知,处理器通过caching行将数据导入caching,例如,在我的Atom处理器上,无论实际读取数据的大小如何,一次只能导入约64个字节。

我的问题是:

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

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

这是哪个?

如果包含正在加载的字节或字的高速caching行尚未存在于高速caching中,则CPU将请求在高速caching线边界处开始的64个字节(最低地址低于所需的64字节的倍数) 。

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

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

相比之下,L1高速caching中的命中负载使用等待时间为3或4个周期,在现代x86 CPU上,存储重载的存储转发等待时间为4或5个周期。 在其他架构上的事情是相似的。

进一步阅读:Ulrich Drepper的每个程序员应该知道的内存 。 软件预取build议有些过时:现代硬件预取程序更聪明,超线程比P4时代更好(因此预取线程通常是浪费)。 此外, x86标签wiki还有很多性能链接。

如果高速caching线的宽度是64字节,那么它们对应的内存块的起始地址可以被64整除。任何地址的低6位都是高速caching行的偏移量。

因此,对于任何给定的字节,通过清除地址的最不重要的六位来find必须被提取的高速caching行,这对应于舍入到可被64整除的最近地址。

虽然这是通过硬件完成的,但我们可以使用一些参考Cmacros定义来显示计算结果:

#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) 

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

主存储器的访问时间大约为70ns到100ns(DDR4稍微快一点)。 这个时间基本上是查找L1,L2和L3caching,并且比命中内存(发送命令到内存控制器,将其发送到内存条),等待响应并完成。

100ns意味着大约200个滴答声。 所以基本上,如果一个程序总是会错过每个内存访问的caching,那么CPU将花费大约99.5%的时间(如果它只是读内存),空闲等待内存。

为了加快速度,有L1,L2,L3caching。 他们使用直接放置在芯片上的存储器,并使用不同types的晶体pipe电路来存储给定位。 由于CPU通常采用更先进的技术生产,并且L1,L2,L3存储器中的生产故障有可能使CPU变得毫无价值(缺陷),所以这比主存储器占用更多的空间,更多的能量和更高的成本大的L1,L2,L3caching增加了错误率,降低了直接降低ROI的收益率。 所以在可用caching大小方面存在巨大的折衷。

(目前,为了能够停用某些部分以减less实际生产缺陷是高速缓冲存储器区域整体上导致CPU缺陷的机会,创build更多的L1,L2,L3高速caching)。

给出一个计时的想法(来源: 访问caching和内存的成本 )

  • L1caching:1ns到2ns(2-4个周期)
  • L2caching:3ns到5ns(6-10个周期)
  • L3caching:12ns至20ns(24-40周期)
  • RAM:60ns(120个周期)

由于我们混合使用了不同的CPUtypes,所以这些只是估计值,但是提供了一个很好的概念,当内存值被提取时,实际上会发生什么,并且我们可能在某个caching层中有一个命中或未命中。

所以caching基本上加速了内存访问(60ns vs. 1ns)。

获取一个值,将其存储在caching中以便重新读取它对于经常访问的variables是有利的,但是对于内存拷贝操作来说,它仍然会变慢,因为只是读取一个值,将值写入某个地方并且从不读取值再次…没有caching命中,死缓(除了这可能发生并行,因为我们有乱序执行)。

这个内存拷贝是如此重要,以至于有不同的手段来加速它。 在早期的时候,内存通常能够复制CPU之外的内存。 它由内存控制器直接处理,所以内存复制操作不会污染caching。

但是除了普通的内存拷贝之外,其他的内存串行访问也是相当普遍的。 一个例子是分析一系列的信息。 有一个整数和计算总和,平均,平均,甚至更简单find一个特定的值(filter/search)是另一个非常重要的一类algorithm运行在任何通用CPU上。

所以通过分析内存访问模式,很明显数据是按顺序读取的。 如果一个程序读取索引i的值,程序也会读取i + 1的值。 这个概率略高于相同程序也读取值i + 2等等的概率。

所以给定一个内存地址,它是(现在仍然)是一个好主意,提前读取并获取额外的值。 这就是为什么有一个升压模式。

在升压模式下的存储器访问意味着一个地址被发送并且多个值被顺序地发送。 每发送一个附加值只需要约10ns(甚至更低)。

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

例如,64字节的高速caching行意味着存储器被划分为大小为64字节的不同(不重叠)的存储器块。 64字节表示每个块的起始地址具有最低的六个地址位总是零。 因此,每次发送这6个零位不需要为任何数量的地址总线宽度(欢迎效果)增加64次的地址空间。

高速caching行解决的另一个问题(旁边的读取和地址总线上的六位保存/释放)是缓冲区的组织方式。 例如,如果高速caching将被分成8个字节(64位)块(单元),则需要存储该存储器单元的地址,该高速caching单元与其一起保存值。 如果地址也是64位,这意味着高速caching大小的一半被地址消耗,导致100%的开销。

由于高速caching行为64字节,而CPU可能使用64位–6位= 58位(不需要太多存储零位)意味着我们可以caching64字节或512位,开销为58位(开销为11%)。 实际上存储的地址甚至比这更小,但是有状态信息(就像caching行有效和准确,脏,需要在RAM中写回等)。

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

特别是当同步不同虚拟内核之间的高速caching/内存访问,每个内核独立的多个处理单元以及最后一个主板上的多个处理器(其中存在多达48个处理器以及更多的板卡)时。

这基本上是目前的想法,为什么我们有caching线。 阅读提前带来的好处是非常高的,而从caching行读取单个字节并且从不再读取其他字节的最坏情况是非常渺茫的,因为这个概率非常渺茫。

高速caching行(64)的大小是在较大的高速caching行之间明智地select的折衷,使得它不太可能在不久的将来读取其最后一个字节,即获取完整高速caching行从内存(并将其写回)以及高速caching组织和高速caching和内存访问的并行化。

处理器可能具有多级caching(L1,L2,L3),这些caching在大小和速度上有所不同。

然而,为了理解每个caching究竟到底是什么,你必须研究那个特定处理器所使用的分支预测器,以及你的程序的指令/数据是如何对付它的。

阅读有关分支预测器 , CPUcaching和replace策略的信息 。

这不是一件容易的事。 如果在一天结束的时候你只想要一个性能testing,那么你可以使用像Cachegrind这样的工具。 但是,由于这是一个模拟,其结果可能在一定程度上有所不同。

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