我知道处理器通过缓存线将数据带入缓存,例如,在我的Atom处理器上,无论实际读取的数据大小如何,每次都会带来大约64字节的数据。
我的问题是:
想象一下,您需要从内存中读取一个字节,哪64个字节将被带入缓存?
我可以看到的两种可能性是,64字节从感兴趣的字节下面最近的64字节边界开始,或者64字节以某种预定的方式分布在字节周围(例如,一半在下面,一半在上面,或全部在上面)。
到底是哪一个?
我知道处理器通过缓存线将数据带入缓存,例如,在我的Atom处理器上,无论实际读取的数据大小如何,每次都会带来大约64字节的数据。
我的问题是:
想象一下,您需要从内存中读取一个字节,哪64个字节将被带入缓存?
我可以看到的两种可能性是,64字节从感兴趣的字节下面最近的64字节边界开始,或者64字节以某种预定的方式分布在字节周围(例如,一半在下面,一半在上面,或全部在上面)。
到底是哪一个?
当前回答
我不能肯定地说,因为每个硬件都是不同的,但它通常是“64字节从下面最近的64字节边界开始”,因为这对CPU来说是一个非常快速和简单的操作。
其他回答
首先,主存访问是非常昂贵的。目前一个2GHz的CPU(最慢的一次)每秒有2G个周期。一个CPU(现在的虚拟核心)可以每tick一次从它的寄存器中获取一个值。由于虚拟核心由多个处理单元(ALU -算术逻辑单元,FPU等)组成,如果可能的话,它实际上可以并行处理某些指令。
一次主存的访问成本约为70ns到100ns (DDR4略快)。这一次基本上是查找L1、L2和L3缓存,然后敲击内存(将命令发送到内存控制器,它将其发送到内存银行),等待响应并完成。
100ns意味着大约200滴答。所以基本上,如果一个程序总是错过每个内存访问的缓存,CPU将花费大约99.5%的时间(如果它只读取内存)空闲等待内存。
In order to speed things up there is the L1, L2, L3 caches. They use memory being directly placed on the chip and using a different kind of transistor circuits to store the given bits. This takes more room, more energy and is more costly than the main memory since a CPU is usually produced using a more advanced technology and a production failure in the L1, L2, L3 memory has the chance to render the CPU worthless (defect) so large L1, L2, L3 caches increase the error rate which decreases the yield which directly decreases ROI. So there is a huge trade off when it comes to available cache size.
(目前创建更多的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等值的概率略高。
因此,给定一个内存地址,提前读取并获取额外的值是一个好主意(现在仍然是)。这就是为什么会有一个加速模式。
boost模式中的内存访问意味着,发送一个地址和多个值依次发送。每个额外的值发送只需要大约额外的10ns(甚至更少)。
另一个问题是地址。发送地址需要时间。为了对较大的内存进行寻址,必须发送较大的地址。在早期,这意味着地址总线不够大,不能在一个周期(tick)内发送地址,并且需要多个周期来发送地址,从而增加了延迟。
例如,64字节的缓存行意味着内存被划分为不同的(不重叠的)64字节大小的内存块。64字节意味着每个块的起始地址的最低6个地址位始终为零。因此,每次发送这6个零位并不需要为任意数量的地址总线宽度增加64倍的地址空间(欢迎效应)。
缓存线解决的另一个问题(除了提前读取和在地址总线上保存/释放6位)是缓存的组织方式。例如,如果缓存将被划分为8字节(64bit)块(单元),则需要存储存储单元的地址,该缓存单元同时保存值。如果地址也是64位的,这意味着缓存大小的一半被地址消耗,导致开销为100%。
由于缓存线是64字节,而CPU可能使用64位- 6bit = 58bit(不需要存储零位),这意味着我们可以缓存64字节或512bit,开销为58bit(11%开销)。实际上,存储的地址甚至比这个还要小,但有状态信息(比如缓存线是否有效和准确,是否脏,是否需要写回ram中等)。
另一个方面是我们有集关联缓存。不是每个缓存单元都能存储某个地址,而只能存储这些地址的一个子集。这使得必要的存储地址位更小,允许并行访问缓存(每个子集可以访问一次,但独立于其他子集)。
特别是当涉及到在不同的虚拟核心之间同步缓存/内存访问时,它们每个核心独立的多个处理单元,最后一个主板上的多个处理器(主板上有多达48个处理器)。
这就是为什么我们要有高速缓存线。提前读取的好处是非常高的,最坏的情况是从缓存线中读取一个字节,然后不再读取其余的字节,因为这种可能性非常小。
cache-line的大小(64)是一个明智的选择,在更大的cache-line之间进行权衡,使得它的最后一个字节在不久的将来也不太可能被读取,从内存中获取完整的cache line所需的时间(并将其写回来),以及缓存组织的开销以及缓存和内存访问的并行化。
如果高速缓存线是64字节宽,那么它们对应的内存块的起始地址可以被64整除。任何地址中最不有效的6位都是缓存线上的偏移量。
因此,对于任何给定的字节,必须读取的缓存行可以通过清除地址中最不重要的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)
如果包含你正在加载的字节或单词的缓存行还没有出现在缓存中,你的CPU将请求从缓存行边界开始的64个字节(你需要的64的倍数下的最大地址)。
现代PC内存模块一次传输64位(8个字节),在8个传输的爆发中,因此一个命令从内存触发一个完整的缓存行读或写。(DDR1/2/3/4 SDRAM突发传输大小可配置为64B;cpu将选择突发传输大小来匹配它们的缓存线大小,但64B是常见的)
根据经验,如果处理器不能预测内存访问(并预取),检索过程可能需要~90纳秒,或~250个时钟周期(从CPU知道地址到CPU接收数据)。
相比之下,在现代x86 cpu上,L1缓存中的命中具有3或4个周期的加载-使用延迟,而存储-重新加载具有4或5个周期的存储-转发延迟。在其他体系结构上情况类似。
进一步阅读:Ulrich Drepper的《每个程序员都应该知道关于内存的事》。软件预取建议有点过时:现代HW预取器更智能,超线程比P4天要好得多(所以预取线程通常是浪费)。而且,x86标签维基有很多关于该体系结构的性能链接。
我不能肯定地说,因为每个硬件都是不同的,但它通常是“64字节从下面最近的64字节边界开始”,因为这对CPU来说是一个非常快速和简单的操作。
处理器可能有多级缓存(L1, L2, L3),它们在大小和速度上有所不同。
然而,要了解每个缓存中到底有什么,你必须研究特定处理器使用的分支预测器,以及程序的指令/数据如何与之对应。
阅读分支预测器,CPU缓存和替换策略。
这不是一项容易的任务。如果在一天结束的时候你只想要一个性能测试,你可以使用像Cachegrind这样的工具。但是,由于这是一个模拟,其结果可能在一定程度上有所不同。