gem5
Table of Contents
1. gem5
1.1. cpu model
https://www.gem5.org/documentation/general_docs/cpu_models/O3CPU
https://carrv.github.io/2017/papers/roelke-risc5-carrv2017.pdf
gem5 的 cpu 分为几种:
atomic
每条指令一个 cycle, 访存时没有时间的概念
timing
atomic 的基础上访存时有时间的概念
minor
timing 的基础上每条指令的 cycle 和 FU 有关, 有流水线, 但是 in-order
o3
在 minor 的基础上支持 out-of-order
一个不太准确的模拟速度的对比:
+------+ ->20 +--------+ ->3 +--------+ ->10 +---------+ ->10 +--------+ | rtl |------------| o3 |-----| atomic |-------+ fpga |------| qemu | +------+ +--------+ +--------+ +---------+ +--------+
其中 `A ->n B` 指 B 比 A 快 n 倍
1.2. O3PipeView
https://www.gem5.org/documentation/general_docs/cpu_models/visualization/
https://github.com/shioyadan/Konata
通过 O3PipeView, 可以看到:
- O3 cpu 支持超标量
- 分支预测失败导致的指令 flush
- icache 对指令 fetch 的影响
- dcache 对 load/store 的影响
- 数据依赖对流水线的影响
- ROB 对流水线的影响
1.3. memory model
https://www.gem5.org/documentation/general_docs/memory_system/
https://github.com/sunwayforever/gem5
$> gh repo clone sunwayforever/gem5 $> cd <path_to_gem5>/configs/toy $> make gem5 $> make hello_cache
有几种不同的 memory 工作模式:
- atomic
- functional
- timing
其中前两者没有时间的概念, 例如 slave port 的 `recvFunctional(pkt)` 会直接以 nested function call 的形式调用到底层的 membus, 通过参数的直接 pkt 返回结果.
而 timing 有时间的概念, slave port 收到 `recvTimingReq(pkt)` 后需要在收到底层回复后通过 `sendTimingResp` 返回结果
functional 可以用来做一些和模拟本身无关的一些访存动作, 例如 load elf
hello_cache 的代码展示了一个简单的 cache 的工作过程:
- cpu_side 通过 `recvTimingReq` 收到来自 cpu 的请求
- cpu_side 通过 simple_cache 把请求通过 mem_side 的 `sendTimingReq` 发送给 membus
- 过一段时间后 mem_side 的 `recvTimingResp` 收到 membus 的回复
- mem_side 通过 simple_cache 把回复通过 cpu_side 的 `sendTimingResp` 把回复发送经 cpu
对于 slave port (cpu_side), 函数的命名为 `recv..Req`, `send..Resp`
对于 master port (mem_side), 函数的命名为 `send..Req`, `recv..Resp`
1.3.1. contention
另外, 在 timing 模式下, port 与外界的交互需要处理有 contention 的情况
1.3.1.1. cpu_side
cpu_side 收到 `recvTimingReq` 时, 可以返回 false 表示暂时无法处理该请求. 但 cpu 并不会自动重发请求: 需要 cpu_side 通过 `sendRetryReq` 通过 cpu 重新发送 `recvTimingReq`
cpu_side 通过 `sendTimingResq` 时, cpu 可以返回 false 表示无法处理, 同样的, cpu_side 不会主动重发 `sendTimingResq`, 而是需要通过 cpu 发送的 `recvRespRetry`
1.3.1.2. mem_side
mem_side 通过 `sendTimingReq` 时, membus 返回 false, mem_side 需要等待 membus 的 `recvReqRetry`
membus 通过 `recvTimingResq` 时, mem_side 返回 false, mem_side 需要发送 `sendRetryResp` 给 membus
1.3.1.3. 总结
+-----------------------+ +-----------------------+ | cpu_side | | mem_side | | +------------------+ | | +------------------+ | +-----+ | | recvTimingReq | | +--------+ | | sendTimingReq | | +--------+ | | | | --------------> | | | | | | -------------> | | | | | |-+->| sendRetryReq +-+->| +-+->| recvReqRetry |-+->| | | cpu | | | <------------- | | | simple | | | <------------- | | | | | | | +------------------+ | | cache | | +------------------+ | | membus | | | | +------------------+ | | | | +------------------+ | | | | |<+--| sendTimingResp | | | |<+--| recvTimingResp |<+--| | +-----+ | | <-------------- +<+--| | | | <------------- | | | | | | recvRespRetry | | +--------+ | | sendRetryResp | | +--------+ | | --------------> | | | | -------------> | | | +------------------+ | | +------------------+ | +-----------------------+ +-----------------------+
1.4. checkpoint
gem5 可以在执行过程中产生 checkpoint, 后续可以从 checkout 恢复执行. checkpoint 会包含系统状态(例如寄存器值) 和内存映像.
一个典型的使用 checkpoint 的场景是: gem5 使用较快的 atomic cpu 启动 linux, 然后保存 checkpoint. 后续可以直接使用 checkpoint 启动 gem5, 避免漫长的 linux 启动过程.
还有一个场景通过采样的方式加快 benchmark 的执行: 先完整执行一次 benchmark, 但每隔一段 tick N 保存一次 checkpoint. 后续再次执行 benchmark 时并行的从这些 benchmark 启动, 但可以只执行 N/2 个 tick (或者每隔一段 tick N 执行一次?), 达到均匀采样 (uniform sampling) 执行的效果. 后面提到的simpoint 也需要配置 checkpoint 工作
1.4.1. 创建 checkpoint
m5.instantiate() exit_event = m5.simulate(10000000) m5.checkpoint("/tmp/1.ckpt")
1.4.2. 使用 checkpoint
m5.instantiate("/tmp/1.ckpt") exit_event = m5.simulate()
1.5. simpoint
Automatically Characterizing Large Scale Program Behavior
https://cseweb.ucsd.edu/~calder/simpoint/
https://xiangshan-doc.readthedocs.io/zh_CN/latest/tools/simpoint/
simpoint 不是均匀采样: 每一段 tick 根据其执行的代码 (根据 BBV 来判断) 被赋予不同的 cluster, 根据每个 cluster 的大小做为权重来采样执行.
simpoint 的基本作法是:
- 先用模拟器完整执行一次 workload, 假设一共执行了 N 条指令, 平均分为 K 份, 每份 N/K 条指令, 记为 T_i
- 统计 T_i 的 BBV (basic block vector), 即程序的 M 个 basic block 在执行 T_i 时分别进入了多少次, 是一个长度为 M 的向量
- 对 BBV_i 降维后, 使用 BBV_i 的欧氏距离对 T_i 进行 k-means 聚类. BBV_i 与 BBV_j 相似意味着 T_i 和 T_j 在执行类似的代码
- 使用最终得到的 k 个聚类中心 (C_i) 加权后 (每个聚类的大小做为权重) 理论上可以代表原始的 T, 例如原始 T 为 `T1,T2,T3,T4,T5,….`, 现在变成 `T2,T2,T3,T4,T3,…`, 其中 T2, T3, T4 是聚类中心
- 生成聚类中心对应的 checkpoint ckpt_i
- 后续需要重复模拟时, 只需要加载 ckpt_i 后执行 C_i, 再配合 C_i 的权重即可估计模拟的结果
1.5.1. 产生 simpoint 数据
# NOTE: gem5 只有 atomic cpu 支持 simport system.cpu = RiscvAtomicSimpleCPU() # NOTE: 20 是指 sample 间隔是每 20 条 inst system.cpu.addSimPointProbe(20) # NOTE: memory 也必须是 atomic 模式 (而不是 timing 模式) system.mem_mode = "atomic"
通过 `make simple_simport` 产生 `m5out/simpoint.bb.gz`, 其内容为:
# NOTE: 格式为 T[:BB_id:count]* T:1:1 :2:3 :3:12 :4:15 T:5:12 T:6:1 :7:4 :8:63 T:9:4 T:10:3 T:11:2 ...
使用 SimPoint3.2 产生 sim points 和 weights (SimPoint 3.2 年代久远, 需要修改一些编译错误)
$> <path_to_simpoint>/bin/simpoint -maxK 5 -loadFVFile m5out/simpoint.bb.gz -inputVectorsGzipped \ -saveSimpoints m5out/simpoints.txt -saveSimpointWeights m5out/weights.txt $> cat m5out/simpoints.txt 43 0 10 1 173 2 87 3 187 4 $> cat m5out/weights.txt 0.0803859 0 0.472669 1 0.0321543 2 0.144695 3 0.270096 4
`43 0` 是指第 0 个 cluster 的聚类中心是第 43 次 sample, 即第 43x20 条指令处, 权重为 0.0803859
1.5.2. 使用 simpoint 产生 checkpoint
最后使用 simpoints.txt 让 gem5 产生 checkpoint:
# NOTE: x20 是因为 sample 间隔是 20 条指令 start_insts = [x * 20 for x in [10, 43, 87, 173, 187]] system.cpu.simpoint_start_insts = start_insts # ... for i, _ in enumerate(start_insts): # NOTE: 无法直接在 m5.simulate 指定 tick 来 exit , 因为不知道 inst 对应的 # tick, 只能依赖设置 system.cpu.simpoint_start_insts 让它在 start inst 处 # exit exit_event = m5.simulate() if exit_event.getCause() == "simpoint starting point found": ckpt_dir = f"m5out/ckpt.{i}" m5.checkpoint(ckpt_dir)
1.5.3. 使用 checkpoint
# NOTE: 执行 20 条 inst 后退出 system.cpu.simpoint_start_insts = [20] m5.instantiate("m5out/ckpt.0") exit_event = m5.simulate()
1.6. 其它
1.6.1. tick/cycle
gem5 默认的 simFreq 是 1x10^12 tick/s, 即每个 tick 相当于 1ps 模拟时间 (而非实现时间). 所以 system.clk_domain.clock=`1GHz` 时每个 cycle 等于 1000 个 tick, system.clk_domain.clock=`2GHz` 时每个 cycle 等于 500 个 tick. 以上信息通过 m5out/stats.txt 可以看到
1.6.2. insn decoder
gem5 使用自定义的 DSL 来生成指令的 decoder 代码, 例如 decoder.isa 中关于 addi 的定义:
... decode OPCODE { 0x03: decode FUNCT3 { format IOp { 0x0: addi({{ Rd_sd = Rs1_sd + imm; }}); 0x2: slti({{ Rd = (Rs1_sd < imm) ? 1 : 0; }}); ... } } ...
表示 addi 的 opcode 为 0x03, FUNCT3 为 0x0
这个 isa 编译后会生成几个相关的 inc 文件给 c 代码使用:
decode-method.cc.inc
case 0x0: // IOp::addi(['\n Rd_sd = Rs1_sd + imm;\n '],{}) return new Addi(machInst); break;
exec-ns.cc.inc
// IOp::addi(['\n Rd_sd = Rs1_sd + imm;\n '], // {}) Fault Addi::execute(ExecContext *xc, trace::InstRecord *traceData) const { int64_t Rd = 0; int64_t Rs1 = 0; Rs1 = xc->getRegOperand(this, 0); Rd = Rs1 + imm; { RegVal final_val = Rd; xc->setRegOperand(this, 0, final_val); if (traceData) { traceData->setData(intRegClass, final_val); } }; return NoFault; }
另外, isa 中还包含了 functional unit 信息, 例如 IntDivOp, 在模拟时 minor 和o3cpu 会选择 fu 并插入对应的 latency (opLat)