【问题标题】:How do cache lines work?缓存行如何工作?
【发布时间】:2011-04-25 03:31:22
【问题描述】:

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

我的问题是:

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

我可以看到的两种可能性是,要么 64 字节从感兴趣字节下方最近的 64 字节边界开始,要么 64 字节以某种预定方式分布在字节周围(例如,一半下,一半以上,或以上全部)。

这是什么?

【问题讨论】:

标签: memory caching line processor


【解决方案1】:

处理器可能具有多级缓存(L1、L2、L3),它们的大小和速度各不相同。

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

了解branch predictorCPU cachereplacement policies

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 可以肯定地说。任何合理的高速缓存设计都会有大小为 2 的幂的行,并且自然对齐。 (例如 64B 对齐)。 它不仅快速简单,而且实际上是免费的:例如,您只需忽略地址的低 6 位。 缓存通常对不同的地址范围执行不同的操作。 (例如,缓存关心标签和索引以检测命中与未命中,然后仅使用缓存行中的偏移量来插入/提取数据)
    【解决方案3】:

    如果包含您正在加载的字节或字的缓存线尚未出现在缓存中,您的 CPU 将请求从缓存线边界开始的 64 个字节(您需要的最大地址是多个64)。

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

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

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

    进一步阅读:Ulrich Drepper 的What Every Programmer Should Know About Memory。软件预取建议有点过时:现代硬件预取器更智能,超线程比 P4 时代要好得多(因此预取线程通常是一种浪费)。此外, 标签 wiki 有很多关于该架构的性能链接。

    【讨论】:

    • 这个答案完全没有意义。 64 位内存带宽(在这方面也是错误的)与 64 字节(!)有什么关系?如果你击中 Ram,10 到 30 ns 也是完全错误的。对于 L3 或 L2 缓存可能是这样,但对于更像 90ns 的 RAM 则不然。你的意思是突发时间——在突发模式下访问下一个四字的时间(这实际上是正确答案)
    • @MartinKersten:DDR1/2/3/4 SDRAM 的一个通道确实使用 64 位数据总线宽度。整个高速缓存行的突发传输确实需要八次传输,每次传输 8B,这就是实际发生的情况。通过首先传输包含所需字节的 8B 对齐块来优化该过程可能仍然是正确的,即从那里开始突发(如果它不是突发传输大小的前 8B,​​则回绕)。不过,具有多级缓存的现代 CPU 可能不再这样做了,因为这意味着将突发的第一个块提前中继到 L1 缓存。
    • Haswell has a 64B path between L2 and L1D cache(即完整的高速缓存行宽度),因此传输包含所请求字节的 8B 会导致该总线的使用效率低下。 @Martin 关于必须进入主内存的负载的访问时间也是正确的。
    • 很好的问题是关于数据是否一次一直向上到内存层次结构,或者 L3 是否在开始将其发送到 L2 之前等待内存中的一整行。不同级别的缓存之间存在传输缓冲区,并且每个未完成的未命中要求一个。因此(完全猜测)可能 L3 将来自内存控制器的字节放入它自己的接收缓冲区,同时将它们放入需要它的 L2 缓存的适当加载缓冲区。当该行从内存中完全传输完毕后,L3 通知 L2 该行已准备好,并将其复制到自己的数组中。
    • @Martin:我决定继续编辑这个答案。我认为它现在更准确,而且仍然简单。未来的读者:另见 Mike76 的问题和我的回答:stackoverflow.com/questions/39182060/…
    【解决方案4】:

    如果高速缓存行是 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)
    

    【讨论】:

    • 我很难理解这一点。我知道是 2 年后,但你能给我这个示例代码吗?一两行。
    • @Nick 这种方法起作用的原因在于二进制数字系统。 2 的任何幂都只设置了一个位并且所有剩余的位都被清除,所以对于 64,你有 0b1000000,请注意最后 6 位数字是零,所以即使你有一些数字与这 6 个集合中的任何一个(代表数字 % 64),清除它们将为您提供最近的 64 字节对齐内存地址。
    【解决方案5】:

    首先,主存访问非常昂贵。目前,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 缺陷的机会)。

    给出一个计时思路(来源:costs to access caches and memory

    • 一级缓存:1ns 到 2ns(2-4 个周期)
    • 二级缓存:3ns 到 5ns(6-10 个周期)
    • L3 缓存:12ns 到 20ns(24-40 个周期)
    • RAM:60ns(120 个周期)

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

    所以缓存基本上可以大大加快内存访问速度(60ns vs. 1ns)。

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

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

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

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

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

    boost 模式下的内存访问是指发送一个地址并顺序发送多个值。每个额外的值发送只需要大约额外的 10ns(甚至更低)。

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

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

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

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

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

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

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

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

    【讨论】:

    • 集合关联缓存使用一些地址位来选择一个集合,因此标签可以比您的示例更短。当然,缓存还需要跟踪集合中哪个标签与哪个数据数组一起使用,但通常集合多于集合中的路数。 (例如 32kB 8 路关联 L1D 缓存,具有 64B 行,在 Intel x86 CPU 中:偏移 6 位,索引 6 位。标签只需要 48-12 位宽,因为 x86-64(目前)只有 48-位物理地址。我相信您知道,低 12 位是页面偏移量并非巧合,因此 L1 可以是 VIPT 而无需别名。)
    • 惊人的答案芽......任何地方都有“喜欢”按钮吗?
    猜你喜欢
    • 2016-02-15
    • 2014-10-06
    • 1970-01-01
    • 2016-09-08
    • 2015-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-02
    相关资源
    最近更新 更多