缓存线如何工作?

Nor*_*wap 152 memory caching processor line

据我所知,处理器通过缓存线将数据带入缓存,例如,在我的Atom处理器上,无论读取的实际数据大小如何,一次都会带来大约64个字节.

我的问题是:

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

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

这是什么?

Eug*_*ith 115

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

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

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

相比之下,L1缓存中的命中具有3或4个周期的负载使用延迟,并且存储重新加载在现代x86 CPU上具有4或5个周期的存储转发延迟.其他架构上的情况类似.

进一步阅读:Ulrich Drepper的每个程序员应该了解的内存.软件预取建议有点过时了:现代硬件预取器更聪明,超线程比P4天好(因此预取线程通常是浪费).此外,标记wiki具有许多针对该体系结构的性能链接.

  • @MartinKersten:一个DDR1/2/3/4 SDRAM通道使用64位数据总线宽度.整个高速缓存行的突发传输确实需要8个8B的传输,并且实际发生的是.通过首先传输包含所需字节的8B对齐的块,即在那里开始突发(并且如果它不是突发传输大小的第一个8B,则包围)来优化该过程可能仍然是正确的.具有多级高速缓存的现代CPU可能不再那样做了,因为它意味着将早期的突发的第一个块中继到L1高速缓存. (4认同)
  • 这个答案完全没有意义。64 位内存带宽(在这方面也是错误的)与 64 字节(!)没有什么关系?如果你击中了 Ram,10 到 30 ns 也是完全错误的。对于 L3 或 L2 缓存可能是这样,但对于更像是 90ns 的 RAM 则不然。你的意思是突发时间 - 在突发模式下访问下一个四字的时间(这实际上是正确的答案) (2认同)
  • [Haswell 在 L2 和 L1D 缓存之间有 64B 路径](http://www.realworldtech.com/haswell-cpu/5/)(即完整的缓存线宽),因此传输包含请求字节的 8B 将有助于该总线的低效使用。@Martin 关于必须进入主内存的负载的访问时间也是正确的。 (2认同)
  • 关于数据是否一次全部到达内存层次结构,还是L3在开始将其发送到L2之前是否等待内存中的完整行,这是一个很好的问题。在不同级别的高速缓存之间有传输缓冲区,每个未完成的未命中都声明一个。因此,(*总猜测数*)L3可能会将来自内存控制器的字节放到其自己的接收缓冲区中,同时又将它们放入需要它的L2高速缓存的适当加载缓冲区中。当该行从内存中完全传输完后,L3通知L2该行已准备就绪,并将其复制到其自己的阵列中。 (2认同)
  • @Martin:我决定继续编辑这个答案。我认为它现在更准确,而且仍然简单。未来的读者:另见 Mike76 的问题和我的回答:http://stackoverflow.com/questions/39182060/why-isnt-there-a-data-bus-which-is-as-wide-as-the-cache-line -尺寸 (2认同)

Kaz*_*Kaz 20

如果高速缓存行是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)
Run Code Online (Sandbox Code Playgroud)

  • @Nick 这种方法有效的原因在于二进制数系统。任何 2 的幂只设置一位并且所有剩余位都被清除,因此对于 64,您有“0b1000000”,请注意最后 6 位数字为零,因此即使您有一些数字与这 6 位中的任何一个(代表数字 % 64),清除它们将为您提供最接近的 64 字节对齐的内存地址。 (2认同)

Mar*_*ten 18

首先,主存储器访问非常昂贵.目前,2GHz CPU(最慢的一次)每秒有2G滴答(周期).CPU(现在的虚拟核心)可以每个tick从其寄存器中获取一次值.由于虚拟核心由多个处理单元(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 vs. 1ns).

获取一个值,将其存储在缓存中以便重新读取它对于经常访问的变量是有益的,但对于内存复制操作,它仍然会变慢,因为只读取一个值,将值写入某处并且永远不会读取值再次...没有缓存命中,死慢(旁边可以并行发生,因为我们已经无序执行).

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

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

因此,通过分析存储器访问模式,很明显,数据是经常按顺序读取的.如果程序读取索引i处的值,则程序也很可能读取值i + 1.该概率略高于同一程序也将读取值i + 2的概率,依此类推.

因此,给定一个内存地址,提前读取并获取其他值是一个好主意.这就是升压模式的原因.

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

另一个问题是地址.发送地址需要时间.为了解决大部分存储器,必须发送大地址.在早期,它意味着地址总线不足以在单个周期(tick)中发送地址,并且需要多个周期来发送地址增加更多延迟.

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

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

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

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

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

这基本上是我们为什么有缓存行的想法.提前读取的好处是非常高的,从缓存线读取单个字节并且再也不读取其余字节的最坏情况非常微小,因为概率很小.

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


jwe*_*ich 6

处理器可能具有多级高速缓存(L1,L2,L3),这些高速缓存的大小和速度不同.

然而,要了解每个缓存的确切内容,您必须研究该特定处理器使用的分支预测器,以及程序的指令/数据如何对其进行处理.

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

这不是一件容易的事.如果在一天结束时你想要的只是性能测试,你可以使用像Cachegrind这样的工具.但是,由于这是模拟,其结果可能在某种程度上不同.