引言

大家是如何写程序的?

++--------------------------------------------------------------+
|+--------------------------------------------------+           |
||+---------------------------------+               |           |
|||+----------------+               |               |           |
||||                |               |               |           |
vvvv                |N  Y           |N   Y          |N   Y      |
读代码 => 写代码 => 写完? --> 编译 => 正确? --> 运行 => 正确? --> 下一个功能

大家有没有想过

  • 如何提升读代码的效率?
  • 如何提升写代码的效率?
  • 如何提升编译的效率?
  • 如何提升运行的效率?
  • 如何提升调试的效率?

本次课内容: 提升开发/调试效率的工具和基础设施

代码开发的基础设施

提升读代码的效率

很少人用记事本写代码

  • 不方便啊

 

没错, 你需要一个方便的编辑器!

 

仅仅会用还不够, 你还需要学习一些高级功能

  • STFW找高级教程吧

提升写相似代码的效率

  • 正则表达式
    • 你用INSTPAT实现了50条指令, 突然想批量修改
  • 记录回放
    • 你定义了50个常量, 突然想全部+1
:%! seq 1 50 | shuf | cat -n | awk '{print "\#define X" $1 " " $2}'
$q1<C-a>jq # 将"本行数字加1"(<C-a>)和"光标移动到下一行"(j)记录到名字为"1"的宏中
49@1       # 将名字为"1"的宏回放49次

你也许很少遇到类似场景, 使得这个例子看上去没什么用

更本质地: 尝试把用编程取代重复劳动的思想应用在其他场景(文本处理/数据处理)

  • 正则表达式编程/shell命令编程/vim命令编程

提升写不相似代码的效率

这没法加速吧…

 

AI帮你写代码 - Copilot

还有ChatGPT

 

当然对学习来说, 这是违反学术诚信的

提升编译的效率

  • 一键编译 - make
    • 你肯定不愿意手工输入每一条gcc命令

还能再快一些吗?

 

  • 并行编译 - make -j $(nproc)
    • 充分利用多核资源
    • 虚拟机可以配置核心的数量
  • 不想每次都输入-j $(nproc)
    • alias make="make -j $(nproc)"
    • 或者export MAKEFLAGS="-j $(nproc)"
    • 或者在.bashrc中加入上述语句

还能再快一些吗?

提升编译的效率(2)

  • 分布式编译 - icecream
    • 把源文件分派给其他机器(你得有账号)编译

 

  • 编译缓存 - ccache
    • 很简单的思想 - 记录.c + 编译选项 -> .o的关系
      • 如果.c之前用相同的选项编译过, 就直接取出.o, 跳过gcc的编译
    • 对源文件很多的项目有明显效果
      • FCEUX(约200个源文件), newlib C库(约650个源文件, PA3中后期)
      • 采用parallel build时, 用verilator编译双核配置的某版本香山, 产生1008个.cpp文件

提升运行的效率

  • Linux小白
    • 打开编辑器 -> 写代码 -> 关闭编辑器 -> gcc ... -> ./a.out
  • 开两个窗口的Linux小白
    • 在窗口1写代码, 在窗口2编译运行
  • 会用一点工具的Linux用户
    • tmux管理多个窗口
    • 写个Makefile, 键入make run自动编译运行
  • 专业的Linux用户
    • 给编辑器绑定快捷键, 实现 “一键”编译运行
  • 资深(懒癌晚期)的Linux用户
    • 监视源文件, 更新时自动触发指定脚本, 实现保存后 “零键”编译运行
    • inotifywait - 这些课件就是这样生成的

基础设施的意义 - 1. 提升效率

香山从改一行Chisel代码到verilator仿真跑起来, 涉及大量基础设施

  • 用mill缓存Scala的编译结果
  • 用Scala-based Firrtl Compiler编译Firrtl
  • 借助NFS在高主频服务器上编译.v
  • 借助NFS在多核服务器上编译.cpp
  • 还有clang, ccache等工具…

即使这样, 在过去也要将近1小时

 

现在有的工具升级了:

  • chisel5及以上只支持MLIR-based Firrtl Compiler, 可并行编译Firrtl
  • verilator的新版本也大大加速了将.v编译成.cpp的时间

现在只需要1~2分钟即可完成上述流程

DDL是死的, 不想办法提升效率, 就无法按时交付项目

基础设施的意义 - 2. 保持注意力集中

很多同学觉得, 10s -> 1s的提升, 意义不大

 

脑科学研究表明: 人脑的短期记忆容量是有限的(7±2个单元)

  • 等1s, 基本上可以持续工作
  • 等10s, 你就开始想别的了
  • 等30s, 你大概率去摸手机了
    • 摸一下5min就过去了

 

可以持续工作的时间才是高质量的时间

  • 来自社畜的肺腑之言

基础设施的意义 - 3. 树立工具意识

使用工具/搭建基础设施, 都是短期投入, 长期回报

 

很多同学宁愿等100次10s, 也不愿意花3分钟设置一下ccache

  • 百度那么不靠谱, 不想STFW
  • man里面那么多英文, 不想RTFM
  • 都已经过去5s了, 下次再说吧

说白了就是懒

  • 哪天要等1小时, 你也不会去想办法了

 

克服惰性才能成长 - 习惯也是要锻炼的

  • 敢想 - 我觉得这里应该要做得更好(需要有点完美主义)
  • 敢做 - 这件事大概率已经有人做了, 我去STFW看看怎么做

Differential Testing

在CPU上执行正确的程序, 结果错

问题: 怎么调试?

 

看波形? printf?

  • dummy, sum, add这些, 不是很难
  • hello-str有几千条指令, 慢慢看还行
  • microbench的test规模跑几十万条指令, 笑容逐渐消失
  • 超级玛丽根本不会停止, @^&!%$#@…

  • CPU设计好玩吗?
  • 好玩
  • 真的吗?

为什么这时候看波形效率这么低?

调试CPU = 找到第一条行为不正确的指令

但是波形/printf并不告诉你哪条指令开始出错, 你要自己找

  • 你不仅要在很多指令里面找
  • 而且还要自己判断每条指令对不对

 

明明是你在调bug, 有时却分明感觉是bug在调你

明明是你在玩游戏,有时却分明感到是游戏在玩你
                                      -- 金山游侠广告词

 

你需要一个 “金山游侠”来 “体会那一技必杀的神奇”

回顾: 防御性编程 - assert

将预期的正确行为直接写到程序中

  • 如果违反断言, 程序马上终止
  • 避免非预期情况继续传播, 造成更难理解的错误
  • 能够大幅提升调试效率

 

跑程序出错 = 执行到最后人工assert

想要的 “金山游侠” = 提前自动assert

  • 能不能在每条指令执行之后插入一个特殊的assert?
    • 如果assert失败, 就找到了第一条出错的指令!

 

听上去很棒!

  • 但这个特殊assert具体应该怎么写?

回顾 - 计算机系统是个状态机

  • 状态集合\(S = \{<R, M>\}\)
    • \(R = \{PC, x_0, x_1, x_2, \dots\}\)
      • RISC-V手册 -> 2.1 Programmers’ Model for Base Integer ISA
      • \(PC\) = 程序计数器 = 当前执行的指令位置
    • \(M\) = 内存
      • RISC-V手册 -> 1.4 Memory
  • 激励事件\(E = \{指令\}\)
    • 执行PC指向的指令
  • 状态转移规则\(next: S \times E \to S\)
    • 指令的语义(semantics)
  • 初始状态\(S_0 = <R_0, M_0>\)

只需要检查这个状态机的状态是否正确

  • 那怎么知道状态机的状态是否正确呢?

Differential Testing

核心思想: 对于符合相同规范的两种实现, 给定有定义的相同输入, 两者行为应当一致

 

Differential Testing的应用:

  • SSL/TLS的实现(规范 = 网络协议)
  • 编译器(规范 = 语言标准)
  • Java反编译(规范 = Java Byte code语义)
  • 安全策略API(规范 = API语义)
  • 文件系统的实现(规范 = VFS语义)
  • 航空航天容错(三副本同时执行, 少数服从多数)

借鉴DiffTest测试CPU

对于符合RISC-V规范的两种实现, 输入正确的程序, 两者的状态变化应当一致

  • 其中一种实现是CPU(DUT, Design Under Test)
  • 另一种选简单的就行, NEMU/Spike/QEMU(REFerence)

 

添加以下API

API 说明
difftest_memcpy() 在DUT和REF间拷贝内存
difftest_regcpy() 在DUT和REF间拷贝寄存器
difftest_exec(n) 让REF执行n条指令
difftest_init() 初始化REF
while (1) {
  cpu_exec(1);
  difftest_exec(1);
  cpu_getreg(&r1);
  difftest_regcpy(&r2, FROM_REF);
  assert(r1 == r2);
}

 

  • 先用Spike来测NEMU, 再用NEMU来测NPC
    • 绝大部分同学都不会去读Spike代码的 😂

DiffTest的意义

  • DiffTest = 在线指令级行为验证方法
    • 在线 = 边跑程序边验证
    • 指令级 = 执行的每条指令都验证
  • 把任意程序转化为指令级别的测试, 对执行每条指令后的状态进行断言
    • 支持不会结束的程序, 例如超级玛丽, OS
  • 无需提前得知程序的结果
    • 检查指令执行的行为, 而不是程序的语义

 

  • riscv-torture通过比较signature(最终的寄存器状态)来判断执行结果
    • 比较signature = 离线程序级行为验证方法
    • 本质上是最后自动assert, 要找到出错的指令还是很困难
    • 如果程序不能结束, 就无法比较

DiffTest的八卦

PA的黑暗时代

PA初代(2014年问世)只有x86可以选

尽管我们做了不少努力, 但编译器还是会偶尔编译出一些复杂指令

  • 例如循环移位, 字符串操作等指令
  • 有时候库里还会出现浮点指令
  • 相比之下RISC-V的模块化特性可控多了

大家深受x86的折磨

高手在民间

为了在残酷的PA中存活下来, 同学们发明了 “自顶向下的替换调试法”

  1. 找个已经通过测试的大腿要代码
  2. 将大腿的.c逐个替换进来
    • 首次可以跑通测试的.c中有bug
  3. 然后将大腿的这个.c中的函数逐个替换进来
    • 首次可以跑通测试的函数中有bug
  4. 其实还能进行语句级别的替换, 但大部分同学还是有底线的
    • “我真的没抄, 只是用来定位bug而已”

其实这是一个可以发顶级论文的想法

2017年: 起源, 加入南京大学PA实验

  • 2017年5月的一节计算机系统综合实验课, jyy和大家一起头脑风暴
    • jyy提到应该想办法让CPU和另一个东西进行cross check(交叉对比)
  • 6月, yzh突发奇想, 让x86的NEMU和真机对比指令行为
    • 通过fork()系统调用创建一个进程, 执行相同的二进制文件, 通过ptrace()控制真机单步执行并获取寄存器状态
    • 还真通过了一些简单的测试, 但因32位和64位区别, 其他测试不通过
  • 7月, 尝试改用QEMU当REF, 通过所有测试

  • 8月, 更新讲义时发现cross check是一个国际象棋术语
    • jyy想到differential testing

2018年: 助力南大乱序CPU NOOP获龙芯杯第二名

首个采用DiffTest验证CPU的FPGA项目

  • 先实现mips32的NEMU, 再作为REF和NOOP对比

 

2018年: 助力南大乱序CPU NOOP获龙芯杯第二名

书写了一周正确实现一个乱序发射乱序执行处理器, 成功运行自制分时多任务操作系统Nanos和仙剑奇侠传的神话

按位取反之前忘记零扩展

 

可惜其中一名队员摸鱼严重, 最后Linux只起了一半

2019年: 助力首期 “一生一芯”果壳处理器硅前验证

2019年: 助力首期 “一生一芯”果壳处理器硅前验证

学生代表作报告介绍果壳项目时, DiffTest均作为关键技术进行介绍

2020年7月CRVS论坛报告

果壳调试Linux时, 在DiffTest的强力帮助下, 两天修复12个bug, 以至于没有留下印象深刻的bug

2020年9月RISC-V全球论坛报告

DiffTest捕捉到在Debian上运行GCC时的bug, 1分钟后定位到原因: 跨页指令取指缺页处理不正确

2020年: 助力开源高性能RV处理器香山硅前验证

香山在开发早期(第二周)就搭建DiffTest框架, 之后便一直开启

【包云岗】香山:开源高性能RISC-V处理器 - 第一届RISC-V中国峰会

2021年: 香山团队实现支持多核的DiffTest

【王凯帆、王华强】SMP-DiffTest:支持多处理器的差分测试方法 - 第一届RISC-V中国峰会

新增memory checker的assert

SMP DiffTest报告了新的硬件bug, 修复后香山成功启动多核Linux

2022年: 龙芯杯引入DiffTest帮助学生调试处理器

2022龙芯杯第三期线上培训

2022年: 香山团队的DiffTest工作被体系结构国际顶会MICRO录用

Yinan Xu, et al. Towards Developing High Performance RISC-V Processors Using Agile Methodology

DiffTest开创了新的处理器验证方法

年份 项目 效果
2017 南京大学PA实验 大幅降低了调试指令bug的难度
2018 南京大学
参加龙芯杯
一周正确实现乱序发射乱序执行处理器, 并运行自制分时多任务操作系统和复杂应用仙剑奇侠传
2019 首期 “一生一芯”
果壳处理器
5天成功启动Linux运行Busybox, 4天成功启动Debian运行GCC/QEMU
2020 开源高性能
RISC-V处理器香山
项目启动后第3周成功运行coremark, 第5周成功运行仙剑奇侠传, 第3个月成功启动Linux, 第4个月成功启动Debian
2021 开源高性能
RISC-V处理器香山
成功启动SMP Linux; 环境就绪后首次上FPGA即可正确跑完所有SPEC 2006 REF测试, 无需在板卡上调试任何处理器相关的bug
2022 龙芯杯 LoongArch赛道引入DiffTest帮助学生调试处理器, 团体赛不少启动Linux的队伍自发使用DiffTest
2022 香山团队 DiffTest工作被体系结构国际顶会MICRO录用

  • 没用过的不知道, 用过的都说好!
  • 但其实软工领域很早就提出这些方法了 😂
    • 所以应该多向软工领域学习

总结

香山项目的基础设施

2021年RISC-V中国峰会, 工具类报告占香山团队报告总量55%(12/22)

  • 大厂里面的基础设施更丰富

基础设施决定大项目成败

  • 学会使用正确的工具做正确的事情
    • 读/写代码 -> 编辑器及其高级功能
    • 代码编译和管理 -> make
    • 终端分屏 -> tmux
    • 加速编译 -> ccache, icecream
    • 自动编译运行 -> inotifywait
    • 调试代码 -> trace, DiffTest, gdb, …
  • 工具不够用的时候, 学会改进/制造新工具
    • 这是从课程大作业迈进复杂项目的必经之路
      • “每个人都能起Linux”并不是梦
      • 当你抱怨没有xxx功能的时候, 你有想过自己去实现吗?
    • 从小事做起 - 理解框架代码中的每一处细节