前言
这章也是重点
话不多说 上知识^^
一、高速缓存的结构和工作原理
(一)几个缓存设计原则
- 每一个缓存项都要包含“标签” (ID)和“内容”
- 通过对有效位置建立索引和约束规则判断给定的数据是否被缓存
- 未命中时,通常要选择一个现有缓存项,并被新缓存项所替代,称之为“替换策略”
- 在写入时,需要传播更改的内容,或将该缓存项标记为“脏”( 直写与回写)
这些都会在之后提及
(二)CPU高速缓存
以CPU中的高速缓存为例,这个也是重点 要知道三种不同的高速缓存类型
1.CPU高速缓存
- CPU高速缓存是一个小型的,快速的,基于SRAM技术的存储器,由硬件自动控制(对用户透明),用于保存频繁访问的主存数据块
- CPU首先在缓存(例如,L1、L2和L3)中查找数据,然后才会在主存中查找数据(就是按照已有的存储器结构)
如图
2.通用的高速缓存组织结构(S E B )(重点)
- 缓存容量:C = S x E x B bytes
S为组
E为行
组包含行 行包含块
B为每块的字节数
每一块里面有有效位以及标记位(tag) 以及数据
3.读高速缓存
首先我们拿到一段字地址
我们将其分为三段(标记 组索引 以及块内偏移量)
(1)确定组的位置
我们通过组索引找到对应的组
(2)检查是否有组中的行能够匹配标记
匹配 且 行的有效位有效 : 命中
1)直接映射高速缓存
每组里面只有一行
- 因此直接与该行匹配,同时进行有效位和标记位的匹配,如果命中则可以继续
- 如果标记未匹配:原有的行会被回收并替换
下面是一个例子
给定一个字地址
我们假设
- 4-bit addresses
M=16 bytes
B=2 bytes/block,
S=4 sets
E=1 Blocks/set
我们跟踪一下每次输入字地址时的命中情况
地址0 ,7,8是冷未命中
地址1命中是因为地址0未命中后加载到了高速缓存中
第二个地址0是因为地址8未命中后加载到了组0中,产生了冲突未命中(中间两位是组索引)
下面是高速缓存的情况
2)E路组相联高速缓存
假设E = 2(行数)
- 同时对两组进行比较
-如果没有成功匹配- 选择当前组中的一行被回收和替换
- 替换策略:随机,最近最少使用
下面是例子
给定一个字地址(组索引为1 因为只有两组)
假设:
- 4-bit addresses
M=16 bytes(块大小)
B=2 bytes/block,
S=2 sets
E=2 Blocks/set
我们跟踪一下每次输入字地址时的命中情况
同样的 可以得到
0,7是冷未命中
1和第二个0命中
8冲突未命中
下面是高速缓存的情况
3)全相联高速缓存(只有一个组)
- 全相联映射允许每一个内存块装入高速缓存中的任意行,称为全相联高速缓存。全相联高速缓存只包含一个缓存组,并由所有高速缓存行组成
(3)根据偏移量定位数据在行中的起始位置
如图
直接映射高速缓存
E路组相联高速缓存
(三)如何处理写操作
1.处理写命中的情况
- 直写:立即写入主存
- 回写:至行被替换才写入主存
(直写确保了数据的保存及时 回写提高了效率)
2.处理未命中的情况
- 写分配 (加载至缓存,然后更新缓存中的行)
- 非写分配 (直接写入内存,不加载数据块至缓存)
(写分配提高了效率 非写分配确保了数据的保存)
因此有下表总结
(四)Intel 酷睿 i7处理器高速缓存层次结构
如图
(五)高速缓存的性能指标(重点)
1.未命中率
- 在缓存中找不到所引用的内存数据的百分比(未命中次数/总访问次数)
- 未命中率 = 1 – 命中率
- 一级缓存:3% ~ 10%
二级缓存中这个值非常小(例如:<1%),取决于缓存
容量等因素
2.命中时间
- 将缓存中的行数据发送到处理器所需的时间
- 包括判定该行是否在缓存中的时间
- 一级缓存:1-2个周期
二级缓存:5-20个周期
3.未命中惩罚
- 由于缓存未命中所需要的额外数据访问时间
- 主存:50-200个时钟周期
- 趋势还在扩大!
4.为什么使用未命中率
- 考虑下面的情况:命中时间1个周期,未命中惩罚100个周期
- 平均访问时间 = 未命中率*未命中惩罚 + 命中率 * 命中时间
- 97% hits: 1 cycle + 0.03 * 100 cycles = 4 cycles
99% hits: 1 cycle + 0.01 * 100 cycles = 2 cycles
所以未命中率更能直观地看到倍数上的差距
(六)编写缓存友好的代码
- 减少内部循环中的缓存未命中
- 对变量更好的重复利用(时间局部性)
- 尽量使用步长为1的模式访问存储器(空间局部性)
- 核心思想:通过理解高速缓存的工作机制,量化我们对局部性原理的定性认知
二、高速缓存对软件性能的影响
(一)通过循环重排提高空间局部性特征
1.矩阵的未命中率分析
假设有:
- 每个矩阵是一个n×n的数组,数据类型为double,sizeof(double)==8
- 缓存块大小为32字节(可以容纳4个64位数据)
- 矩阵的维度(n)是一个非常大的值:1/N趋近于0
- 缓存的容量较小,不足以同时缓存矩阵中的多行数据
如下
(1) 矩阵乘法
- n x n 矩阵相乘
- 共O(N3) 次操作(时间复杂度)
- 每个元素都需要进行N次读操作
- 每个目标元素都需要进行N次累加
- 这些累加可能会在寄存器中进行
代码如图
1)C语言数组在内存中的布局
-
C语言数组以“行优先”方式分配内存
每一行的元素在内存中的位置都是连续的 -
在一行中,逐列进行访问
- 访问连续的元素
- 如果块大小B大于8字节,则可利用空间局部性
- 未命中率 = 8/B
-
在一列中,逐行进行访问
- 依次访问元素之间距离很远
- 没有空间局部性!
未命中率 = 1 (即100%)
我们试着按照以上原则改变遍历的次序 并进行未命中率分析 有
2)矩阵乘法(ikj)
3)矩阵乘法(jki)
4)矩阵乘法(kji)
5)矩阵乘法总结
- ijk (& jik):
◼ 2次内存加载,0次存储
◼ 未命中次数/循环 = 1.25 - kij (& ikj):
◼ 2次内存加载,1次存储
◼ 未命中次数/循环 = 0.5 - jki (& kji):
◼ 2次内存加载,1次存储
◼ 未命中次数/循环 = 2.0
(二)通过分块提高时间局部性特征
1.矩阵乘法
假设:
◼矩阵中的元素是double类型
◼缓存块容量是8个double类型数据(64字节)
◼缓存总的容量 C << n (远远小于n)
- 第一次迭代
- n/8 + n = 9n/8 misses (未命中)
-
随后在缓存中
-
第二次迭代
- n/8 + n = 9n/8 misses (未命中)
-
总的未命中次数:9n/8 * n2 = (9/8) * n3
2.分块矩阵乘法
代码如下
这是它的形象遍历
-
假设:
◼矩阵中的元素是double类型
◼缓存块容量是8个double类型数据(64字节)
◼缓存能够容纳三个“块”:3B^2<C -
第一次(块)迭代
- 每个块出现B2/8次未命中
- 有 2n/B * B2/8 = nB/4 次未命中(忽略矩阵C)
-
第二次(块)迭代
- 有 2n/B * B2/8 = nB/4 次未命中
- 有 2n/B * B2/8 = nB/4 次未命中
-
总未命中次数
- nB/4 * (n/B)2 = n3/(4B)
(三)存储器山
1.存储器山
- 读吞吐量(读带宽)
◼从存储系统中读取数据的速率 (MB/S) - 存储器山:一个读吞吐量关于时间和空间局部性的函数关系
◼一种表征存储系统性能的简洁方法
2.测量存储器山的函数
- 使用不同的elems和stride的参数组合,调用
test()函数
◼对于每一个 elems 和 stride 参数
◼首先,调用一次test(),对缓存进行“预热”(因为第一次调用一定是冷未命中)
◼然后,再次调用test(),然后测量读吞吐量
总结
存储器山了解概念即可
高速缓存的结构这部分 要知道三种不同的高速缓存类型 以及 高速缓存的性能指标