前言
事实上模型复杂,包含:寄存器,cache,主存,SSD,磁盘,磁带,网盘。抽象来看,层次储存像是个有着最高层次的速度和最低层次的成本的存储池。
访问所需周期:寄存器,0;cache,4~75;主存,几百;磁盘,几千万;
局部性可以使得在 Core i7 上,使得两个矩阵相乘的速度差40倍。
本章会讲:SRAM,DRAM,ROM,旋转和固态硬盘。着重讲 cache。分析程序局部性,提高局部性,存储山(局部性表示访问时间的函数)。
6.1存储技术
计算机技术的大部分成功源于存储技术巨大的进步。
6.1.1随机访问存储器
RAM 分为易失性 RAM、非易失性 RAM。
易失性 RAM分为 SRAM 和 DRAM。
非易失性 RAM:ROM。
DRAM 分成 d 个超级单元,d = i 行 * j 列。每个超级单元包含 8 位。信息通过引脚流入流出芯片。每个引脚携带一位信号。为了读出超级单元的内容,内存控制器先传送地址 i ,再传送地址 j。行地址 i 称为 RAS (row access strobe),列地址 j 称为 CAS,他们共用一组引脚。
一个实例:CPU 读取地址 A。首先CPU 芯片组上称为总线接口的电路在总线上发起读事务,CPU 把地址 A 发送给内存控制器(包含在被称作 I/O桥的芯片组中)。(内存有多个内存模块组成,每个内存模块又包含 8 个 DRAM 芯片。每个芯片包含 64M 超级单元。)内存控制器会选择包含地址 A 的内存模块,将地址 A 翻译成超级单元地址 i,j 给该内存模块,内存模块广播该超级单元地址给每个芯片,两步传送,先 i 后 j,传送 i 后会将每个芯片的第 i 行放在当前芯片的内部行缓冲区。传送 j 后,会在内部行缓冲区选出超级单元 i,j ,内存模块会将 8 个芯片的超级单元组合成 64 位的目标数据传送给内存控制器(第 0 个芯片存储低八位,第 1 个芯片存储 9-16 位,以此类推),cpu 从系统总线上感知到数据,就读取数据到总线接口,最后把数据复制到寄存器。
非易失性存储器,也叫 rom 。包括:可编程 ROM,可擦可编程 ROM,FLASH,SSD。
6.1.2磁盘存储
磁盘转速从 5400 到 15000 都有。一个盘片两个盘面,盘面由磁道组成,磁道由扇区和格式化的间隙组成。每个扇区包含相等数量数据位组成(一般是 512 字节)。一个柱面指的是所有盘片的对应磁道的集合。
描述随机访问存储器时候,1K 表示 1024,描述 I/O 设备,1K 表示 1000。
磁盘以扇区大小的块来读写,对扇区访问时间由寻道时间,旋转时间,传送时间来决定。
寻道平均时间在 3-9 ms,最多的 20 ms。
旋转平均时间 (60s/RPM)/2,最多是 60s/RPM。
传送一个扇区的平均时间 60s/RPM*(磁道每扇区)。
64bit 存到 SRAM 需要 4ns,经过 DRAM 60ns。512 字节 SRAM 需要 256ns,DRAM 需要 4000ns==4um。
现代磁盘结构复杂,所以磁盘控制器将扇区抽象成逻辑块,负责逻辑块和物理扇区的翻译。
在磁盘存储之前要格式化磁盘(在扇区之间的空隙填充扇区的信息,标志柱面的缺陷,预留柱面等操作),所以格式化后的容量要比最大容量小。
IO 总线(我们所说的IO总线是一个抽象简化的版本,真实的是PCI总线和PCIe总线,因为对程序员来说只是速率的差异,所以不关心细节。)设计与 CPU 底层没啥关系,速度也比内存总线和系统总线慢,但是可以容纳很多设备,比如:USB 控制器,显卡,主机总线适配器(一个在服务器和存储装置间提供 I/O 处理和物理连接的电路板或集成电路适配器)等。
读磁盘数据步骤:CPU 将一个命令,内存地址,逻辑块号写到与磁盘相关联的内存映射IO技术的地址(内存映射io系统中,地址空间中有一块(block)地址为io设备通信而保留,每个这样地址称为一个io端口,当一个设备连接到总线时,它与一个或多个端口相关联),磁盘控制器读扇区并执行DMA传送到内存,当DMA 传送完成,磁盘控制器给CPU发送一个中断。
6.1.3固态硬盘
固态硬盘由块(32-128页)组成,块由页组成。页大小一般是512到4KB。块大小一般是16KB到512KB。以页为单位读写。页所属的块整个被擦除(所有位设为1)后,才能写。十万次重复写之后,块就废了。读写访问时间大约五十微妙左右。
一个实例:Intel 固态硬盘,一直读写直到完全磨损需要十年左右的时间。正常使用需要10000多年。
6.1.4存储技术趋势
DRAM 和磁盘技术进化的比CPU和SRAM慢很多,所以制造商加了很多Cache来缓冲这种差距,非常有效的决定,因为应用程序可以用局部性原理来使用Cache。
6.2局部性
大多数地方都用到局部性,软件硬件都有,比如:操作系统使用内存作为虚拟地址空间中最常用到的块的缓存。
6.2.1对程序数据引用的局部性
for i 内 for j 比 for j 内 for i 更具局部性。
6.2.2取指令的局部性
循环代码具有良好的时间局部性。
6.2.3局部性小结
步长越大,空间局部性越差。
取指令有很好的时间和空间局部性。
循环体越小,循环次数越多,局部性越好。
6.3存储器层次结构
6.3.1存储器层次结构中的缓存
高层缓存用硬件实现,如果用随机放置策略代价会很高,这时候会选择其他更严格的防治策略,但是注意这时候容易发生冲突不命中。
一个嵌套循环的工作集超过了缓存的大小,会发生容量不命中。
大多数情况下,缓存管理由软件、硬件或两者结合自动执行。编译器管理寄存器,cache硬件管理cache,操作系统和地址翻译硬件管理主存,AFS本地客户端管理本地磁盘。
6.3.2存储器层次结构概念小结
6.4高速缓存存储器
6.4.1通用的高速缓存存储器组织结构
主存地址有m位,cache有S个集合,每个集合有E行,每个行有B数据位组成的数据块、1个有效位,t个标记位(内存地址位中的子集,唯一标识存储在行中的块)。
m = b+s+t (s是S的索引)
6.4.2直接映射高速缓存
当程序访问大小是2的次方的数组时候,直接映射高速缓存中通常会发生冲突不命中。
用中间位作为组索引是有原因的。
如果用较高位做索引
情况如上图中的中间所示,连续的块都别映射到了同一个组中(特别的,如果是直接映射高速缓存,连续的块被映射到同一行中)这样的确也能利用缓存,如上图所示,当引用第一个元素的时候,会把第1、2、3、4个拷贝到缓存的组0中,以后对2、3、4的引用就能直接在缓存中提取。引用第5个元素的时候,把第5、6、7、8个拷贝到缓存的组1中,同样的,对4、5、6的引用能直接在缓存中提取。后面的情况类似就不再叙述。
通过上面的叙述,你可能已经发现一个问题:当对缓存的组1进行操作的时候,缓存中的其它组是没有被利用的,这和缓存中只有一个组其实效果是一样的。对缓存中的其他组没有很好的利用,也就是说,虽然也有缓存的利用,但有较大化。
改用中间位做索引,如上图中的右图所示,同一组中的块不再是连续的,这样可以保证缓存中的所有组都能被有效的利用。
引用第1个元素的时候,将把第1、5、9、13放入缓存组1中
引用第2个元素的时候, 把第2、6、10、14放入缓存
引用第3个,把3、7、11、15放入缓存
引用第4个,把4、8、12、16放入缓存
这样对前四个元素的引用都不会命中,而而对后面的引用都能命中。这种过程也就是所谓的缓存预热。
可以看看,但解释的并不清楚。还是下面的例题来的爽:
填充元素到数组可以防止抖动。
6.4.3组相联高速缓存
替换策略书中没有详细说,越底层,不命中代价越高,所以需要更好的策略。
6.4.4全相联高速缓存
适合做小的高速缓存,比如虚拟内存系统中的翻译备用缓冲器TLB,用来缓存页表项。
6.4.5有关写的问题
写分配通常是写回。非写分配通常是直写。
现代系统大多使用写回和写分配,并且这是一种趋势。与读相对称,都利用了局部性。层次越低,越选择写回策略。给我们一个提示,在写代码时候,可以使用写回策略。
6.4.6一个真实的高速缓存层次结构的解剖
i-cache 和 d-cache 可以有不同的块大小,相联度和容量。使用不同高速缓存可以并行,也确保了数据和指令访问不会出现冲突不命中,但可能会引起容量不命中增加。
6.4.7高速缓存参数的性能影响
衡量指标:不命中率(不命中数量/引用数量),命中率,命中时间,不命中处罚(L1不命中需要从L2得到的处罚数10个周期,从L3中50个周期,从主存中200个周期)。
6.5编写高速缓存友好的代码
每次循环迭代会有 min(1, (wordsize*k/B)) 次缓存不命中。缓存块大小是B字节,k是步长。
1. focus on 核心函数中的循环
2. 减小循环内部缓存不命中数量
6.6综合:高速缓存对程序性能的影响
习题:
不命中率25%,增大 Cache 块大小也是这么多,因为冷不命中不可避免。
6.6.1存储器山
当程序的时间局部性很差时,空间局部性能补救,反过来效果不明显,预取机制使得效果明显的多。
存储器系统的性能不是一个数字就能描述的,它是一座时间和空间局部性的山。我们要使得频繁使用的字从 Cache L1中取出,还要使得尽可能多的字从Cache L1中访问得到。
6.6.2重新排列循环以提高空间局部性
每次迭代:加载次数2次,存储次数1次,A未命中0次,B未命中0.25次,C未命中0.25次。
每次迭代内存引用和不命中数量都相同的,有大致相同的测量性能。
与内存访问次数相比,不命中率是个更好的性能预测指标。
虽然工作集远大于Cache,但是因为步长1,使得Intel处理器可以使用预取硬件,最终使得速度可以跟上内循环的内存访问。所以我们要开发出具有良好空间局部性的程序。
6.6.3在程序中利用局部性
1. focus on 核心函数中的循环
2. 按数据在内存中的顺序且步长1的读取数据。
3. 一旦从存储器中读取了数据对象,尽可能多的使用它。
6.7小结
1. 多利用局部性
2. 磁盘整理