How do cache lines work?
我了解到处理器通过缓存线将数据引入缓存,例如,在我的Atom处理器上,缓存线一次会引入大约64个字节,不管实际读取的数据的大小如何。
我的问题是:
假设您需要从内存中读取一个字节,哪64个字节将被放入缓存中?
我能看到的两种可能性是,64个字节要么从相关字节下面最接近的64个字节边界开始,要么64个字节以某种预定的方式(例如,一半在下面,一半在上面,或者全部在上面)分布在字节周围。
这是什么?
如果缓存中还没有包含正在加载的字节或字的缓存线,则CPU将请求从缓存线边界开始的64个字节(低于所需地址的最大地址是64的倍数)。
现代PC内存模块一次传输64位(8字节),八次传输突发,因此一个命令触发从内存中读取或写入一条完整的缓存线。(DDR1/2/3/4 SDRAM突发传输大小可配置为高达64B;CPU将选择突发传输大小以匹配其缓存线大小,但64B是常见的)
根据经验,如果处理器无法预测内存访问(并预取),则检索过程可能需要约90纳秒或约250个时钟周期(从知道地址的CPU到接收数据的CPU)。
相比之下,一级缓存中的命中具有3或4个周期的负载使用延迟,而在现代x86 CPU上,存储重新加载具有4或5个周期的存储转发延迟。其他架构上的情况类似。
进一步阅读:UlrichDrepper是每个程序员都应该知道的关于内存的知识。软件预取建议有点过时:现代硬件预取器更智能,超线程比P4天更好(因此预取线程通常是浪费)。另外,x86标签wiki有许多针对该体系结构的性能链接。
如果缓存线是64字节宽的,那么它们对应于从可被64整除的地址开始的内存块。任何地址的最低有效6位都是缓存线的偏移量。
因此,对于任何给定的字节,必须提取的缓存线可以通过清除地址中最不重要的6位来找到,这对应于取整到可被64整除的最近地址。
虽然这是由硬件完成的,但我们可以使用一些参考C宏定义来显示计算:
1 2 3 4 5 6 7 8 9 | #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(现在的虚拟核心)可以从它的寄存器中每一次获取一个值。由于虚拟核心由多个处理单元(算术逻辑单元、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缺陷的机会)。好的。
给出时间观念(来源:访问缓存和内存的成本)好的。
- 一级缓存:1ns到2ns(2-4个周期)
- 二级缓存:3ns到5ns(6-10个周期)
- 三级缓存:12ns到20ns(24-40个周期)
- 内存:60ns(120个周期)
由于我们混合了不同的CPU类型,这些只是估计值,但给出了一个好主意,当获取内存值时,我们可能会在某些缓存层中遇到命中或丢失。好的。
因此,缓存基本上大大加快了内存访问速度(60ns与1ns)。好的。
获取一个值,将其存储在缓存中以便重新读取它对经常访问的变量是有益的,但是对于内存复制操作,由于只读取一个值,将该值写入某个位置,而不再读取该值,因此仍然会变慢…没有缓存命中,速度非常慢(除此之外,这可能是并行发生的,因为我们有无序的执行)。好的。
这个内存拷贝非常重要,所以有不同的方法来加速它。在早期,内存常常能够在CPU之外复制内存。它是由内存控制器直接处理的,因此内存复制操作不会污染缓存。好的。型
但是除了一份普通的内存拷贝之外,其他的内存串行访问也是很常见的。一个例子是分析一系列信息。拥有一个整数数组和计算和、平均值、平均值甚至更简单的查找某个值(过滤/搜索)是另一类非常重要的算法,每次在任何通用CPU上运行。好的。型
因此,通过分析内存访问模式,可以很明显地看出数据是经常按顺序读取的。如果程序读取索引i处的值,程序也将读取值i+1。这个概率略高于同一个程序读取值i+2等的概率。好的。型
因此,给定一个内存地址,提前读取并获取附加值是一个好主意。这就是为什么有一个助推模式。好的。型
Boost模式下的内存访问意味着发送一个地址,并按顺序发送多个值。每发送一个附加值只需要大约10纳秒(甚至更低)。好的。型
另一个问题是地址。发送地址需要时间。为了寻址大部分内存,必须发送大地址。在早期,这意味着地址总线不够大,无法在一个周期(勾选)内发送地址,并且需要多个周期来发送地址,从而增加了延迟。好的。型
例如,64字节的缓存线意味着内存被划分为大小为64字节的不同(不重叠)内存块。64字节意味着每个块的起始地址的最低六个地址位始终为零。因此,不需要每次发送这六个零位,就可以为任意数量的地址总线宽度增加64倍的地址空间(欢迎效果)。好的。型
缓存线解决的另一个问题(除了提前读取和在地址总线上保存/释放6位之外)是缓存的组织方式。例如,如果一个缓存将被分为8字节(64位)的块(单元),则需要存储存储单元的地址,该缓存单元将保存该值。如果地址也是64位,这意味着一半的缓存大小被地址占用,从而导致100%的开销。好的。型
由于缓存线是64字节,CPU可能使用64位-6位=58位(不需要将零位存储得太右),这意味着我们可以缓存64字节或512位,开销为58位(11%开销)。事实上,存储的地址甚至比这个小,但是有状态信息(比如缓存线有效和准确,脏,需要在RAM中写回等等)。好的。型
另一个方面是我们已经设置了关联缓存。不是每个缓存单元都能存储某个地址,但只能存储其中的一个子集。这使得必要的存储地址位更小,允许并行访问缓存(每个子集可以访问一次,但独立于其他子集)。好的。
尤其是在同步不同虚拟核心之间的缓存/内存访问时,每个核心都有独立的多个处理单元,最后在一个主板上有多个处理器(主板可容纳多达48个处理器及更多)。好的。
这基本上就是我们有缓存线的原因。提前读取的好处是非常高的,而从缓存线中读取一个字节而不再读取其余的字节的最坏情况是非常小的,因为这种可能性非常小。好的。
缓存线(64)的大小是一个明智的选择,在较大的缓存线之间进行权衡,使得它的最后一个字节在不久的将来也不可能被读取,从内存中获取完整缓存线(并将其写回)所需的时间,以及缓存组织中的开销以及缓存和内存访问的并行化。好的。好啊。
处理器可能有多级缓存(l1、l2、l3),这些缓存的大小和速度不同。
然而,要了解每个缓存中到底有什么,您必须研究特定处理器使用的分支预测器,以及程序的指令/数据如何对其进行操作。
了解分支预测器、CPU缓存和替换策略。
这不是一项容易的任务。如果一天结束时你只想做一个性能测试,你可以使用像cachegrind这样的工具。然而,由于这是一个模拟,其结果可能在某种程度上有所不同。
我不能肯定地说,因为每个硬件都是不同的,但它通常是"64字节从下面最接近的64字节边界开始",因为这对CPU来说是一个非常快速和简单的操作。