我们已经了解整个SoC计算机系统如何执行程序了
本次课内容: 体系结构优化
性能优化一直以来是同学们非常向往的环节
但如果你费了九牛二虎之力的优化措施, 反而带来性能倒退, 你是否会感到沮丧?
如何在一个复杂系统里面找到值得优化之处, 是一个正经的科学问题
不同的应用场景 -> 性能收益的趋势不同 -> 需要不同的代表性程序 -> 不同的benchmark
一些需求:
microbench是一个不错的选择
需要分析运行时间受哪些因素影响
给定一个程序, 性能优化的方向
inst/prog
- 即减少动态指令数, 可在仿真环境中统计
cycle/inst
(CPI) - 即增加IPC, 统计周期数后可计算得到
time/cycle
- 即增加主频, 可查看综合报告获得
统计得到的IPC还是无法指导我们如何优化IPC
/--- frontend ---\ /-------- backend --------\
+-----+ <--- 2. computation efficiency
+--> | FU | --+
+-----+ +-----+ | +-----+ | +-----+
| IFU | --> | IDU | --+ +--> | WBU |
+-----+ +-----+ | +-----+ | +-----+
^ +--> | LSU | --+
| +-----+
1. instruction supply ^
3. data supply --+
将处理器分为前端和后端, 要提升处理器的执行效率:
性能事件(performance event): 在电路层次与性能指标相关的具体事件
统计性能事件发生的频次: 性能计数器(performance counter)
相对于IFU取到指令, 我们更关心IFU什么时候/为什么取不到指令
性能瓶颈究竟在哪里? 哪些优化工作是值得做的? 优化工作的预期性能收益是多少?
Amdahl’s law可以告诉我们答案:
The overall performance improvement gained by optimizing a single part of a system is
limited by the fraction of time that the improved part is actually used.
优化系统中某部分所获得的总体性能收益, 受限于那部分实际使用的时间占比.
公式表示: 假设系统某部分实际使用的时间占比是p, 该部分在优化后的加速比是s, 则整个系统的加速比为
f(s) = 1 / (1 - p + p / s)
例: 某程序的运行过程分为独立的两部分A和B, 其中A占80%, B占20%
1/(0.8+0.2/5)=1.1905
1/(0.8+0.2/5000)=1.2499
1/(0.2+0.8/2)=1.6667
这是一则来自软件工程领域的忠告
启发: 不能依靠直觉来优化处理器设计, 觉得哪里有优化机会就改哪里
Amdahl’s law = 小学数学应用题
性能计数器的统计结果指导我们得出优化方案
访存延迟是一个需要考虑的问题:
需要校准访存延迟, 使其接近真实芯片的情况
有同学认为FPGA肯定比仿真 “先进”, 但应该结合使用场景来分析
一名合格的处理器架构师应该具备如下能力:
体系结构设计能力 != RTL编码能力, 一些反例:
体系结构设计能力只能通过实践来锻炼 -> 理解并跨越教科书的边界
access time /\ capacity price
/ \
~1ns / reg\ ~1KB $$$$$$
+------+
~10ns / DRAM \ ~10GB $$$$ (20元/GB)
+----------+
~10ms / disk \ ~1TB $$ (固态0.683元/GB, 机械0.139元/GB)
+--------------+
~10s / tape \ >10TB $ (0.033元/GB)
+------------------+
集成并组织多种存储器, 在整体上达到容量大, 速度快, 成本低的效果
架构师发现, 程序对内存的访问存在若干规律
这些现象和程序的结构和行为有关
我们的生活中也存在局部性原理
即使慢速存储器容量大, 程序在一段时间内访问的存储单元相对集中
存储层次结构的诀窍: 将各种存储器按层次排列, 上层存储器速度快但容量小, 下层存储器容量大但速度慢
例: 16GB DRAM + 4TB 机械硬盘
access time /\ capacity price
/ \
~1ns / reg\ ~1KB $$$$$$
+------+
-----> ~3ns / cache \ ~30KB $$$$$
+----------+
~10ns / DRAM \ ~10GB $$$$
+--------------+
~10ms / disk \ ~1TB $$
+------------------+
~10s / tape \ >10TB $
+----------------------+
关键思想: 在寄存器和DRAM之间加一层存储器
冷知识: cache发音同cash
为了方便描述, 将从DRAM读入的数据称为一个数据块
设计cache需要解决如下问题
先考虑指令缓存icache(instruction cache), 暂不考虑写操作
addr/4
)k
个cache块,
则将内存地址为addr
的数据块读入编号为(addr/4)%k
的cache块
复位时cache块均无效, 需要通过有效位(valid)标识(与tag统称元数据)
icache的工作流程:
不同情况做不同的事情 = 状态机!
能不能让工具帮我们自动寻找会出错的输入?
在给定约束条件下寻找可行解的数学工具(类似解方程组或线性规划)
Z3是一个SMT(Satisfiability Modulo Theories, 可满足性模理论)求解器
一类基于求解器的验证方法
assert()
不成立
把上述内容转换成求解器识别的语言, 让求解器寻找是否存在可行解
assert(cond1)
和assert(cond2)
,
则尝试求解是否存在输入使得!cond1 || !cond2
成立
只要能跑出来, 都是好消息! (复杂设计需要花很长时间)
assert()
,
从而证明了正确性!Chisel的测试框架chiseltest能将FIRRTL代码翻译成Z3识别的语言,
让Z3证明给定assert()
是否正确
Verilog可以使用SymbiYosys来进行形式化验证
import chisel3._
import chisel3.util._
import chiseltest._
import chiseltest.formal._
import org.scalatest.flatspec.AnyFlatSpec
class Sub extends Module {
val io = IO(new Bundle {
val a = Input(UInt(4.W))
val b = Input(UInt(4.W))
val c = Output(UInt(4.W))
})
io.c := io.a + ~io.b + Mux(io.a === 2.U, 0.U, 1.U)
val ref = io.a - io.b
assert(io.c === ref)
}
class FormalTest extends AnyFlatSpec
with ChiselScalatestTester with Formal {
"Test" should "pass" in {
verify(new Sub, Seq(BoundedCheck(1)))
}
}
BoundedCheck()
参数用于指定SMT求解器需要证明的周期数
BoundedCheck(4)
表示让SMT求解器尝试证明DUT在复位之后的4个周期内,
在任意输入信号下都不违反assert()
BoundedCheck(1)
即可
可归纳出出验证顶层模块的伪代码
class CacheTest extends Module {
val io = IO(new Bundle {
val req = new ...
val block = Input(Bool())
})
val memSize = 128 // byte
val mem = Mem(memSize / 4, UInt(32.W))
val dut = Module(new Cache)
dut.io.req <> io.req
val dutData = dut.io.rdata
val refRData = mem(io.req.addr)
when (dut.io.resp.valid) {
assert(dutData === refData)
}
}
一些需要补充的细节:
0
mem
中读出数据
assert()
检查
需要有个评价指标来评估优化的效果
AMAT = p * access_time + (1 - p) * (access_time + miss_penalty)
= access_time + (1 - p) * miss_penalty
p
- 命中率access_time
- 访问时间,
即从接收请求到得出命中结果所需的时间miss_penalty
- 缺失代价, 此处即访问DRAM的时间
缓存优化的方向:
access_time
,
但在架构设计上可优化的空间不多p
miss_penalty
提升命中率 = 降低缺失率, 需要先了解cache的缺失有哪些原因
计算机科学家Mark Hill在其1987年的博士论文中提出3C模型, 刻画了cache缺失的3种类型:
每个数据块都可存放到任意cache块中
(tag % 组数)
中的任一个cache块w
个cache块, 则称为w
路组相联 31 m+n m+n-1 m m-1 0 n = log2(cache块总数/w)
+---------+---------+--------+
| tag | index | offset | index: 组索引
+---------+---------+--------+
w
不大时, CAM的开销可以接受
w=1
为直接映射, w=cache块总数
为全相联
给SDRAM的访问时间建立如下的简单模型
+------------------------ arvalid有效
| +-------------------- AR通道握手, 接收读请求
| | +------------ 状态机转移到READ状态, 并向SDRAM颗粒发送READ命令
| | | +------ SDRAM颗粒返回读数据
| | | | +-- R通道握手, 返回读数据
V a V b V c V d V
|---|-------|-----|---|
a+b+c+d
4(a+b+c+d)
与SDRAM颗粒类似, AXI总线也支持 “突发传输”(burst transfer)
axburst
和axlen
信号指示,
具体细节RTFM a b c d
|---|-------|-----|---| <-------------------- 第1个节拍
|-----|---| <-------------- 第2个节拍
|-----|---| <-------- 第3个节拍
|-----|---| <-- 第4个节拍
与重复4次的总线传输事务相比:
3a
3b
3d
综上, 采用突发传输所需开销为a+b+4c+d
,
可节省3(a+b+d)
的开销
缓存有那么多参数, 那么多策略, 怎么选合适?
评估指标: IPC, 主频, 面积, …
不同参数的组合情况太多了
在评估cache的性能表现时, 通常我们会选取指定的benchmark
TMT = 缺失次数 * 缺失代价
, TMT越小, IPC越大
关于缺失次数的一些观察
缺失代价可以完整评估一次后取平均值
没有必要每次都完整运行程序, 只需要一个简单的功能模拟器cachesim
cachesim的评估效率能比ysyxSoC快几千甚至上万倍!
还能通过cachesim来对icache进行性能测试的DiffTest
在复杂的项目中, 设计空间非常大, 评估一组参数的时间开销耗时也长
如何能快速评估不同设计参数的性能表现, 是一个至关重要的问题
体系结构顶会ISCA多次举行基于ChampSim模拟器的大赛
需求分析 -> 结构设计 -> 逻辑设计 -> 功能验证 -> 性能验证 -> 性能优化 -> 面积评估/时序分析
如果只会写RTL代码, 无法做出一个好的处理器