引言

我们已经了解整个SoC计算机系统如何执行程序了

 

本次课内容: 体系结构优化

  • 性能评估和优化方法
  • 缓存的设计和验证
  • 缓存的优化和设计空间探索

性能评估和优化方法

性能优化方法: 拍脑袋 vs. 方法论

性能优化一直以来是同学们非常向往的环节

  • 看着自己设计的处理器跑得越来越快, 心里充满成就感

 

但如果你费了九牛二虎之力的优化措施, 反而带来性能倒退, 你是否会感到沮丧?

  • 拍脑袋的决策: 在RTL层次正确实现功能 != 性能提升

 

如何在一个复杂系统里面找到值得优化之处, 是一个正经的科学问题

  • 是科学问题, 就有科学的方法
    1. 评估当前的性能
    2. 寻找性能瓶颈
    3. 采用合适的优化方法
    4. 评估优化后的性能, 对比获得的性能提升是否符合预期

性能评估的第一步: 选benchmark

  • 拍脑袋 = 凭直觉, 科学方法 = 量化分析
    • 在系统领域, 用数据说话是基本素养

 

  • 需要一个量化的指标来衡量 “性能”
    • 性能高 = 跑得快 -> 程序的执行时间!

 

  • 程序有很多, 要评估所有程序是不现实的
    • 需要跑有代表性的程序 - 越能代表处理器的应用场景, 就越合适
    • 代表性 = 优化技术在这些程序上带来的性能收益, 与真实应用场景中的性能收益趋势基本一致

 

不同的应用场景 -> 性能收益的趋势不同 -> 需要不同的代表性程序 -> 不同的benchmark

各种各样的benchmark

  • Linpack(高性能计算), MLPerf(机器学习), CloudSuite(云计算), SPEC CPU(通用计算)…
    • SPEC CPU 2006覆盖的领域包括: 垃圾邮件检测, 压缩, 编译, 组合优化, 人工智能搜索, 基因序列搜索, 量子计算, 视频编解码, 以太网协议模拟, 寻路算法, 文本处理, 流体力学, 量子化学, 生物分子, 有限元分析, 线性规划, 影像光线追踪, 计算电磁学, 天气预报, 语音识别…
    • SPEC CPU 2017新增了生物医学成像, 3D渲染和动画, 采用蒙特卡罗树搜索的人工智能围棋程序(很大概率受2016年AlphaGo的影响)

 

  • Coremark和Dhrystone早就该淘汰了 - 图灵奖得主David Patterson
    • 它们代表不了什么应用场景
    • Dhrystone是上世纪80年代的benchmark
  • 今天: 应用程序更新换代, 编译技术日趋成熟, 硬件算力大幅提升
    • 只不过业界惯性太大了, 大部分人都不太懂评测 😂

适合教学的benchmark

一些需求:

  • 规模不算太大, 在模拟器甚至在RTL仿真环境中的执行时间不到2小时
    • SPEC CPU在x86真机上执行都要几个小时, RTL仿真算下来要7年!
  • 可在裸机环境中运行, 无需启动Linux
  • 程序具有一定代表性
    • 不像CoreMark和Dhrystone那样采用合成程序(用代码片段拼接)

 

microbench是一个不错的选择

  • RTL仿真可以采用train规模
  • 作为AM程序, 无需启动Linux即可运行
  • 包含10个子项: 排序, 位操作, 语言解释器, 最大流, 矩阵, 压缩, md5, 素数, A*算法

如何优化运行时间?

需要分析运行时间受哪些因素影响

 

        time      inst     cycle     time
perf = ------- = ------ * ------- * -------
        prog      prog      inst     cycle

 

给定一个程序, 性能优化的方向

  • 减小inst/prog - 即减少动态指令数, 可在仿真环境中统计
    • 优化措施: 改进算法, 编译优化, 采用行为更复杂的指令集
  • 减小cycle/inst(CPI) - 即增加IPC, 统计周期数后可计算得到
    • 优化措施: 体系结构优化(我们关注的主题)
  • 减小time/cycle - 即增加主频, 可查看综合报告获得
    • 优化措施: 电路关键路径优化, 后端物理设计优化

如何提升IPC?

统计得到的IPC还是无法指导我们如何优化IPC

  • 需要分析IPC受哪些因素影响 -> 重新审视处理器如何执行指令
       /--- frontend ---\    /-------- backend --------\
                                  +-----+ <--- 2. computation efficiency
                             +--> | FU  | --+
       +-----+     +-----+   |    +-----+   |    +-----+
       | IFU | --> | IDU | --+              +--> | WBU |
       +-----+     +-----+   |    +-----+   |    +-----+
          ^                  +--> | LSU | --+
          |                       +-----+
1. instruction supply                ^
                    3. data supply --+

将处理器分为前端和后端, 要提升处理器的执行效率:

  1. 前端需要保证指令供给
    • 影响所有指令 - 指令都没有, 整个处理器只能空转
  2. 后端需要保证计算效率和数据供给
    • 大部分计算指令的执行效率取决于相应功能单元, 如乘除法, 浮点等
    • 访存指令的执行效率取决于LSU, 尤其是load指令需要等待数据返回

如何量化评估指令供给, 计算效率和数据供给?

性能事件(performance event): 在电路层次与性能指标相关的具体事件

  • 如果 “IFU取到指令”的事件经常发生, 则说明指令供给能力强
  • 如果 “LSU取到数据”的事件经常发生, 则说明数据供给能力强
  • 如果 “EXU完成计算”的事件经常发生, 则说明计算效率高

 

统计性能事件发生的频次: 性能计数器(performance counter)

  • 可以观察程序在处理器上运行的时间都花在哪里, 相当于对处理器内部做profiling
  • 实现: 检测到事件发生的条件(握手, 控制信号有效等), 就加1

 

相对于IFU取到指令, 我们更关心IFU什么时候/为什么取不到指令

  • 这些 “事件不发生”的新事件更能帮助我们梳理性能瓶颈在哪里

寻找性能瓶颈: 阿姆达尔定律(Amdahl’s law)

性能瓶颈究竟在哪里? 哪些优化工作是值得做的? 优化工作的预期性能收益是多少?

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%

  • 若将B优化5倍, 则加速比是1/(0.8+0.2/5)=1.1905
  • 若将B优化5000倍, 则加速比是1/(0.8+0.2/5000)=1.2499
  • 若将A优化2倍, 则加速比是1/(0.2+0.8/2)=1.6667
<------- A --------><-B->
++++++++++++++++++++ooooo   原程序
++++++++++++++++++++o       将B优化5倍
++++++++++++++++++++        将B优化5000倍, 优化后B的运行时间很短
++++++++++ooooo             将A优化2倍

抛开workload谈优化就是耍流氓

这是一则来自软件工程领域的忠告

  • 也适用于处理器体系结构优化
  • 优化占比50%的部分, 和优化占比5%的部分, 效果天差地别

 

启发: 不能依靠直觉来优化处理器设计, 觉得哪里有优化机会就改哪里

  • 否则很容易采取了一个没有效果的方案, 甚至会在实际场景中造成性能倒退
  • 根据评估数据采取合适的设计方案, 才是科学的做法

 

Amdahl’s law = 小学数学应用题

  • 初学者 “耍流氓” = 缺少相关的专业素养
  • 锻炼专业素养比 “会写RTL”重要得多

校准访存延迟

性能计数器的统计结果指导我们得出优化方案

  • 如果性能计数器的统计结果不准确, 开展优化的实际效果可能不明显, 甚至带来性能倒退
  • 在 “一生一芯”中, 准确 = 接近真实芯片的情况

 

访存延迟是一个需要考虑的问题:

  • ysyxSoC环境是假设处理器和各种外设运行在同一频率下
    • verilator的一个周期 = CPU的一个周期 = 外设的一个周期
  • 受电气特性的影响, 外设通常运行在低频率(100MHz), CPU运行在高频率(1GHz)
    • 按照上述假设得出的仿真结果, IPC是过度乐观的

 

需要校准访存延迟, 使其接近真实芯片的情况

开放讨论: FPGA的作用

有同学认为FPGA肯定比仿真 “先进”, 但应该结合使用场景来分析

 

  1. 作为功能测试的环境: 教学
    • FPGA的作用是加速仿真过程, 不需要校准访存延迟
    • 需要满足: FPGA比特流生成时间 + FPGA运行时间 < verilator编译时间 + verilator运行时间
      • FPGA比特流生成时间 ~ 小时量级, verilator编译时间 ~ 分钟量级
      • 因此verilator运行时间需要达到小时量级, 才能加速
  2. 作为性能测试的环境, 同时也作为目标平台: 比赛或科研项目
    • 不需要校准访存延迟
  3. 作为性能测试的环境, 但目标是流片: 企业产品研发, “一生一芯”学习
    • 期望通过FPGA得出的性能数据尽可能与真实芯片一致
    • 校准访存延迟是不可或缺的

经典体系结构的4类优化方法

  1. 局部性 - 利用数据访问性质提升指令/数据供给的效率. 代表性技术:
    • 缓存, 今天的主题
  2. 并行 - 多个实例同时工作, 提升系统整体的处理能力. 代表性技术:
    • 指令级并行 - 同时执行多条指令: 流水线, 多发射, VLIW, 乱序执行
    • 数据级并行 - 同时访问多个数据: SIMD, 向量指令/向量机, GPU
    • 任务级并行 - 同时执行多个任务: 多线程, 多核, 多处理器, 多进程
  3. 预测 - 先投机, 后检查, 猜对就赚了. 代表性技术:
    • 分支预测
    • 缓存预取
  4. 加速器 - 用专用部件执行特定任务. 代表性技术:
    • 各种定制化的加速器IP
    • 自定义扩展指令
      • 乘除法器 - 将乘除法指令视为基础指令的扩展

重新审视处理器体系结构设计

一名合格的处理器架构师应该具备如下能力:

  • 理解程序如何在处理器上运行
  • 对于支撑程序运行的特性, 能判断它们适合在硬件层次实现, 还是适合在软件层次实现
  • 对于适合在硬件层次实现的特性, 能提出一套在各种因素的权衡之下仍然满足目标要求的设计方案

 

体系结构设计能力 != RTL编码能力, 一些反例:

  • 能将流水线处理器的框图翻译成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)
                +------------------+

集成并组织多种存储器, 在整体上达到容量大, 速度快, 成本低的效果

一个重要的观察 - 局部性原理

架构师发现, 程序对内存的访问存在若干规律

  1. 时间局部性 - 访问某存储单元后, 短时间内可能再次访问它
  2. 空间局部性 - 访问某存储单元后, 短时间内可能访问其相邻存储单元

 

这些现象和程序的结构和行为有关

  • 程序大多数时候顺序执行(空间局部性)或循环执行(时间局部性)
  • 相关的变量在源码中的位置相近, 编译器为其分配相近的存储空间
  • 变量的数量 <= 操作的次数, 因此必定有变量被多次访问(时间局部性)
  • 循环访问数组(空间局部性)

 

我们的生活中也存在局部性原理

  • 关联的物品放在一起(柴米油盐放在厨房) - 空间局部性
  • 经常使用的物品放在身边(手机不离身) - 时间局部性

存储层次结构是局部性原理的重要应用

即使慢速存储器容量大, 程序在一段时间内访问的存储单元相对集中

 

存储层次结构的诀窍: 将各种存储器按层次排列, 上层存储器速度快但容量小, 下层存储器容量大但速度慢

  • 先访问上层存储器, 若命中, 则直接访问当前层级的数据;
  • 若缺失, 则访问下一层级, 下层将目标数据及其相邻数据传递给上层
    • 传递给上层 - 利用了时间局部性
    • 相邻数据 - 利用了空间局部性

 

例: 16GB DRAM + 4TB 机械硬盘

  • 若局部性好, 则综合表现接近4TB DRAM
    • 总价不到900元; 4TB DRAM约80000元
  • 若局部性差, 则综合表现接近4TB 机械硬盘

简易缓存

cache - 寄存器和DRAM之间的存储层次

               access time       /\          capacity    price
                                /  \
                  ~1ns         / reg\          ~1KB     $$$$$$
                              +------+
         ----->   ~3ns       /  cache \        ~30KB     $$$$$
                            +----------+
                  ~10ns    /    DRAM    \      ~10GB     $$$$
                          +--------------+
                  ~10ms  /      disk      \     ~1TB      $$
                        +------------------+
                  ~10s /        tape        \  >10TB       $
                      +----------------------+

关键思想: 在寄存器和DRAM之间加一层存储器

  • 访存时, 先访问cache, 若命中, 则直接访问
  • 若缺失, 则先将数据从DRAM读入cache, 然后再访问cache中的数据
  • 可以提升指令供给和数据供给的效率

 

冷知识: cache发音同cash

设计cache需要考虑的问题

为了方便描述, 将从DRAM读入的数据称为一个数据块

  • cache中存放的数据块称为cache块

 

设计cache需要解决如下问题

  1. 数据块的大小应该是多少?
    • 即以多少数据为单位进行管理?
  2. 如何检查访存请求是否在cache中命中?
  3. cache的容量通常比DRAM小, 如何维护cache块和DRAM中数据块之间的映射关系?
    • cache满了后怎么办?
  4. CPU可能会执行写操作, 从而更新数据块中的数据, cache应如何维护?

简易指令cache的设计

先考虑指令缓存icache(instruction cache), 暂不考虑写操作

 

  • 块大小先取4B
    • 若小于4B, 取一条指令要进行多次访存, 效率太低
    • 大于4B是否更合适, 后续探索

 

  • 标记cache块的唯一ID(称为tag), 来检查是否命中
    • 希望ID的计算足够简单, 易于实现
    • cache块从内存中来 -> 按块大小给对内存地址编号(addr/4)
    • CPU发出访存请求, 只需要对比tag, 即可得知副本是否在cache中

简易指令cache的设计(2)

  • 用简单的直接映射(direct-mapped)方式维护数据块之间的映射关系
    • 假设cache可存放k个cache块, 则将内存地址为addr的数据块读入编号为(addr/4)%k的cache块
    • 多个数据块可能会映射到相同的cache块
      • 根据局部性原理, 将来更大概率访问新块, 故应用新块替换旧的

 

  • 可将访存地址分为以下3部分:
    • tag - 数据块在cache中的唯一ID
    • index - 数据块在cache中的索引
    • offset - 块内偏移, 指示需要访问数据块中的哪部分数据
块大小 = 16B, 可存放4096个cache块

 31     16 15    4 3      0
+---------+-------+--------+
|   tag   | index | offset |
+---------+-------+--------+

 

复位时cache块均无效, 需要通过有效位(valid)标识(与tag统称元数据)

cache的实现

icache的工作流程:

  1. IFU向icache发出取指请求
  2. icache根据请求的index部分索引出一个cache块, 判断其tag与请求的tag是否相同, 并检查该cache块是否有效. 若是, 则命中, 跳转到第5步
  3. 通过总线在DRAM中读出请求所在的数据块
  4. 将该数据块填入相应cache块中, 更新元数据
  5. 向IFU返回取出的指令

 

不同情况做不同的事情 = 状态机!

  • cache的实现 = 块存储 + 元数据存储 + 控制器
    • 一般用SRAM作为存储单元, ASIC流程还涉及SRAM选型等问题
      • 简单起见, 可用触发器作为存储单元
    • 控制器 = 总线状态机的扩展

缓存的验证

测试 vs. 证明

  • 测试 - 判断给定的输入是否能运行正确
  • 证明 - 判断所有的输入是否能运行正确

 

  • DiffTest属于测试
    • 你可能遇到过: cputest都对, 但跑超级玛丽就会出错
  • UVM也属于测试
    • 不要迷信UVM的100%覆盖率报告

 

  • 光靠测试无法证明DUT的正确性, 除非测试用例覆盖了所有输入
    • 耗时长, 可以借助软件测试理论的等价类测试方法降低测试集大小
    • 但这需要人工划分等价类: 需要人工干涉, 就可能会出错

 

能不能让工具帮我们自动寻找会出错的输入?

求解器 - 把问题当作方程来解

在给定约束条件下寻找可行解的数学工具(类似解方程组或线性规划)

 

Z3是一个SMT(Satisfiability Modulo Theories, 可满足性模理论)求解器

  • 求解包含实数, 整数, 比特, 字符, 数组, 字符串等内容的命题是否成立
  • 能将问题表达成一阶逻辑语言的某个子集, 就能让SMT求解器求解
  • 广泛应用于定理自动证明, 程序分析, 程序验证和软件测试等领域
#!/usr/bin/python
from z3 import *

x = Real('x')  # 定义变量
y = Real('y')
z = Real('z')
s = Solver()
s.add(3*x + 2*y - z == 1)    # 定义约束条件
s.add(2*x - 2*y - 4*z == -2)
s.add(-x + 0.5*y - z == 0)
print(s.check())  # 求是否存在可行解: sat
print(s.model())  # 输出可行解: [y = 14/25, x = 1/25, z = 6/25]

还能求解数独

形式化验证 = 把验证结果当作方程来解

一类基于求解器的验证方法

  • 变量: 测试输入
  • 约束条件: DUT
  • 求解目标: 至少一个assert()不成立

 

把上述内容转换成求解器识别的语言, 让求解器寻找是否存在可行解

  • 例如, 若某设计中有assert(cond1)assert(cond2), 则尝试求解是否存在输入使得!cond1 || !cond2成立

 

只要能跑出来, 都是好消息! (复杂设计需要花很长时间)

  • 若可行解存在, 则找到一个可以让DUT出错的反例, 辅助设计者调试
  • 若可行解不存在, 则说明所有输入都不会违反assert(), 从而证明了正确性!

Chisel重磅福利

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)即可

用形式化验证测试icache

  • 有无cache不应该影响访存结果的正确性
    • REF: 不带cache的访存过程
    • assert: 无论有无cache, 读结果应当一致

 

可归纳出出验证顶层模块的伪代码

  • 如果用Verilog, 可以借鉴思路
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
  • 让cache缺失时从mem中读出数据
    • 由于DUT不产生写操作, 因此可与REF使用相同的存储器
  • DUT和REF返回数据的时机不同, 需要同步后再通过assert()检查
    • 又是状态机

缓存的性能优化

cache性能的评价指标

需要有个评价指标来评估优化的效果

  • 缓存技术主要用于提升访存效率, 应该通过访存相关的指标来评价
  • 通常采用AMAT(Average Memory Access Time)作为评价指标
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缺失的3C模型

提升命中率 = 降低缺失率, 需要先了解cache的缺失有哪些原因

 

计算机科学家Mark Hill在其1987年的博士论文中提出3C模型, 刻画了cache缺失的3种类型:

  1. Compulsory miss, 强制缺失
    • 定义: 在一个容量无限大的cache中所发生的缺失
    • 表现: 在第一次访问一个数据块时所发生的缺失
  2. Capacity miss, 容量缺失
    • 定义: 不扩大cache容量就无法消除的缺失
    • 表现: 因cache无法容纳所有所需访问的数据而发生的缺失
  3. Conflict miss, 冲突缺失
    • 定义: 除上述两种原因外引起的缺失
    • 表现: 因多个cache块之间相互替换而发生的缺失

降低缺失率

  • 降低Compulsory miss: 需要在访问某数据块前就将其读入cache中
    • 当前工作流程不支持, 需要添加新机制才能实现: 预取(prefetch)
    • 需要根据profiling结果决定是否值得实现

 

  • 降低Capacity miss: 扩大cache容量
    • 但cache容量并非越大越好, 需要权衡
      • 容量越大, 流片面积越大, 从而增加流片成本
      • 存储阵列越大, 其访问延迟越高, 从而增加cache的访问时间

 

  • 降低Conflict miss: 减少多个cache块之间相互替换的情况
    • 采用新的cache块组织方式, 允许数据块读入到多个cache块中

全相联(fully-associative)组织方式

每个数据块都可存放到任意cache块中

  • 具体位置由替换算法根据每个cache块当前的访问状态决定
    • 替换算法需要选择一个将来最不可能被访问的cache块(预测未来)
  • 常见的替换算法: FIFO, LRU, random…
    31              m m-1    0
   +-----------------+--------+
   |       tag       | offset |
   +-----------------+--------+
  • 代价1: 不需要index位, tag位更长, 元数据的存储阵列更大
  • 代价2: 判断命中时, 需要与所有cache块检查其tag是否匹配
    • CAM(Content-Addressable Memory), 根据内容查询存储地址
    • 一般工艺库不提供这种原语
      • 要么全定制(难度大), 要么用触发器搭(面积, 功耗大)
  • 一般只在cache块数量较少的场景下使用

组相联(set-associative)组织方式

  • 给所有cache块分组, 在组间通过直接映射方式选出一个组, 然后在组内通过全相联方式选出一个cache块
    • 每个数据块都可以存放到组号为(tag % 组数)中的任一个cache块
    • 若每组有w个cache块, 则称为w路组相联
    31    m+n m+n-1   m m-1    0     n = log2(w)
   +---------+---------+--------+
   |   tag   |  index  | offset |   index: 组索引
   +---------+---------+--------+
  • 判断命中时, 只需要与组内所有cache块检查其tag是否匹配即可
    • w不大时, CAM的开销可以接受

 

  • 直接映射和全相联都可以看成是组相联的特例: w=1为直接映射, w=cache块总数为全相联
  • 现代CPU一般采用8或16路组相联

块大小的选择

  • cache块增大
    • ✔️ 降低tag的存储开销
    • ✔️ 能存放更多相邻数据, 更好地捕捉到程序的空间局部性
    • ✖️ 缺失代价上升
    • ✖️ cache块的数量更少, 增加Conflict miss

 

// 2个大小为4的cache块
               1111       2222        cache
|--------------oooo-------oooo-----|  memory, `o`为程序访问的热点数据

// 1个大小为8的cache块
               11111111               cache
|--------------oooo-------oooo-----|  memory

缺失代价的分析

给SDRAM的访问时间建立如下的简单模型

+------------------------ arvalid有效
|   +-------------------- AR通道握手, 接收读请求
|   |       +------------ 状态机转移到READ状态, 并向SDRAM颗粒发送READ命令
|   |       |     +------ SDRAM颗粒返回读数据
|   |       |     |   +-- R通道握手, 返回读数据
V a V   b   V  c  V d V
|---|-------|-----|---|

 

  • 对于单个总线传输事务, 开销是a+b+c+d
  • 若cache块大小是总线数据位宽的4倍, 则缺失代价为4(a+b+c+d)
    • 其中存在优化的机会

总线的突发读写

与SDRAM颗粒类似, AXI总线也支持 “突发传输”(burst transfer)

  • 在一次总线传输事务中包含多次连续的数据传输
  • 通过axburstaxlen信号指示, 具体细节RTFM
  a     b      c    d
|---|-------|-----|---| <-------------------- 第1个节拍
                  |-----|---| <-------------- 第2个节拍
                        |-----|---| <-------- 第3个节拍
                              |-----|---| <-- 第4个节拍

与重复4次的总线传输事务相比:

  • AR通道只需要进行一次握手, 开销可节省3a
  • 若还有地址连续的READ命令, SDRAM控制器的状态机则直接转移到READ状态继续发送, 开销可节省3b
  • 回复R通道的同时可发送下一次READ命令, 开销可节省3d

综上, 采用突发传输所需开销为a+b+4c+d, 可节省3(a+b+d)的开销

  • 这是SDRAM控制器视角的开销, 对CPU来说节省的周期数更多

缓存的设计空间探索

设计空间探索

缓存有那么多参数, 那么多策略, 怎么选合适?

  • 特别地, 会不会有的组合不相容反而导致性能下降?
  • 要想选一个还不错的组合, 需要评估它们效果如何

 

评估指标: IPC, 主频, 面积, …

  • 这些指标的表现都不错, 才是一个比较好的方案
  • 主频和面积可以通过综合工具的报告获得
  • IPC需要在校准过访存延迟的ysyxSoC环境中运行完整程序才能获得

 

不同参数的组合情况太多了

  • 如果每种参数组合都要花数小时才能得到IPC, 效率就很低
  • 为了提升设计空间探索的效率, 一种方式是牺牲IPC的统计精度
    • 通过较低的开销来统计一个能体现IPC变化趋势的指标

快速估算cache的性能表现

在评估cache的性能表现时, 通常我们会选取指定的benchmark

  • 可以统计某程序cache缺失需要等待的总时间TMT(Total Miss Time)
    • TMT = 缺失次数 * 缺失代价, TMT越小, IPC越大

 

关于缺失次数的一些观察

  1. 对于给定的程序, cache的访问次数是固定的
    • 有itrace就可以复现icache访问的过程
    • 不需要仿真整个ysyxSoC, 甚至连NPC都不需要
  2. itrace还包含完整的指令流
    • 只需要指令流的PC值, 不需要指令本身
  3. 对于给定的访存地址序列, cache的缺失次数与访问内容无关
    • 只需要维护元数据即可, 无需维护数据部分

缺失代价可以完整评估一次后取平均值

功能模拟器即可满足需求

没有必要每次都完整运行程序, 只需要一个简单的功能模拟器cachesim

  • 用C代码模拟cache的工作流程
  • 接收指令流的PC序列(简化版itrace), 通过维护元数据统计缺失次数

 

cachesim的评估效率能比ysyxSoC快几千甚至上万倍!

  • 数千秒 vs. 数秒
  • 进一步挖掘多核机器的性能: 命令行参数 + 脚本启动多个cachesim

 

还能通过cachesim来对icache进行性能测试的DiffTest

  • 在NPC中运行一个程序得到的性能计数器结果, 应与cachesim执行相应PC序列统计得到的缺失次数等数据完全一致
    • 相应的性能bug无法通过功能测试的DiffTest或形式化验证发现
      • 例如, 即使icache一直缺失, 程序仍然能在NPC上正确运行

模拟器和体系结构研究

在复杂的项目中, 设计空间非常大, 评估一组参数的时间开销耗时也长

  • 在NPC上跑microbench train规模(几小时) vs. 在香山上跑SPEC CPU ref规模的片段(1周)

如何能快速评估不同设计参数的性能表现, 是一个至关重要的问题

  • 模拟器对体系结构设计来说非常重要

 

  • 香山跑一轮程序: gem5(全系统模拟器) 2小时 vs. verilator 1周
    • 用RTL跑一组配置的时间, 可以用模拟器探索84组不同配置的效果

 

总结

真正的处理器设计流程

需求分析 -> 结构设计 -> 逻辑设计 -> 功能验证 -> 性能验证 -> 性能优化 -> 面积评估/时序分析

  • 我们用icache作为例子贯穿了整个流程
  • 一个不完整的知识链条
    • 需求分析 <- 性能评估 <- benchmark, 访存延迟校准 <- SoC <- NPC
    • 结构设计 <- 经典体系结构的4类优化方法 <- 缓存 <- 总线
    • 逻辑设计 <- 电路, RTL编码
    • 功能验证 <- DiffTest, 形式化验证 <- C语言, 模拟器, 仿真环境
    • 性能验证 <- cachesim <- C语言, 模拟器, 性能计数器
    • 性能优化 <- 设计空间探索 <- C语言, 模拟器, 软件优化, Linux脚本
    • 面积评估/时序分析 <- 电路, yosys-sta

 

如果只会写RTL代码, 无法做出一个好的处理器