diff --git a/README.md b/README.md index 005a4ff..99485a2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ 本项目没有使用任何标准的 Java 依赖管理/构建 工具, 而是使用 Bash 脚本完成构建. Bash 脚本具有简单、透明的优势, 适合本项目这种依赖少而简单, 无复杂构建需求的项目. 并且使用 Bash 脚本便于集成各式各样的工作流, 而不会被构建工具限制. -本项目之功能性测试使用 Docker 容器进行. 整个源码目录被挂载进容器之后在容器内被编译为 class 文件并执行, 依次读取测试数据文件夹 (当前为 `test-data/asm-handmade`) 内的 `*.sy` 文件并输出汇编文件 `*.s`, 随后在容器内被交叉编译器汇编为 ARM 可执行文件, 随后使用 qemu 执行并输出 `main` 返回值. 随着开发的进行将逐步修改测试行为为判断输出是否正确. +本项目之功能性测试使用 Docker 容器进行. 整个源码目录被挂载进容器之后在容器内被编译为 class 文件并执行, 依次读取测试数据文件夹 (当前为 `test-data/asm-handmade`) 内的 `*.sy` 文件并输出汇编文件 `*.s`, 随后在容器内被交叉编译器汇编为 ARM 可执行文件, 随后使用 qemu 执行并输出 `main` 返回值. 用测试脚本判断输出是否正确. ## 开发环境使用 @@ -56,18 +56,15 @@ - `clang`: 使用 clang 编译并运行测试样例 - `clang_O2`: 使用 clang -O2 编译并运行测试样例 -- (未完成) `ssyc_llvm`: 使用我们的编译器编译出 llvm ir, 并使用 llvm-as/llc 转为汇编文件, 随后汇编并运行测试样例 -- (未完成) `ssyc_asm`: 使用我们的编译器编译出汇编文件, 随后汇编并运行测试样例 - -一个输出的例子如下: - -```bash - -``` +- `ssyc_llvm`: 使用我们的编译器编译出 llvm ir, 并使用 llvm-as/llc 转为汇编文件, 随后汇编并运行测试样例 +- `ssyc_asm`: 使用我们的编译器编译出汇编文件, 随后汇编并运行测试样例 +- `ssyc_llvm_long`: 使用我们的编译器编译出 llvm ir, 并使用 llvm-as/llc 转为汇编文件, 随后汇编并运行测试样例, 在出现用例结果错误时不会停止 +- `ssyc_asm_long`: 使用我们的编译器编译出汇编文件, 随后汇编并运行测试样例, 在出现用例结果错误时不会停止 +- `generate_stdout`: 使用 clang -O2 编译并运行用例, 保存运行结果, 相当于构造标准答案 ## 程序本体参数说明 -本程序接收三个参数: ` `. 其中 `target` 参数为输出类型, 可取 `ast`, `ir`, `asm` 三者之一. `input_file` 与 `output_file` 分别为输出与输出文件名, 可以使用 `-` 来令程序使用标准输入/输出. +本程序接收三个参数: ` `. 其中 `target` 参数为输出类型, 可取 `ast`, `llvm`, `asm` 三者之一. `input_file` 与 `output_file` 分别为输出与输出文件名, 可以使用 `-` 来令程序使用标准输入/输出. 例子: @@ -79,7 +76,7 @@ ./m run ast xxx.sy - # 读取 xxx.sy 文件作为输入, 将生成的中间表示输出到 yyy.ir 文件 -./m run xxx.sy yyy.ir +./m run llvm xxx.sy yyy.ir ``` ## 测试结果说明 @@ -87,21 +84,27 @@ 当前程序的测试主要以目测编译后程序的返回值为主. 一个输出的例子为: ``` -$ ./m test -Test test-data/asm-handmade/0-number.sy return 1 -Test test-data/asm-handmade/1-add-sub.sy return 1 -Test test-data/asm-handmade/2-mul-div-mod.sy return 1 -Test test-data/asm-handmade/3-variable.sy return 1 -Test test-data/asm-handmade/4-if.sy return 1 -Test test-data/asm-handmade/5-while.sy return 1 +$ ./m test inst-combine ssyc_asm +[12:03:08] Finish: ANTLR Generation +[12:03:12] Finish: Java compile +================= begin: ssyc ================= + Finish (0/4) 00-multi-add.sy +[12:03:13] Finish (1/4) 01-repeat-add.sy + Finish (2/4) 02-distribute-imul.sy + Finish (3/4) 03-addmulconst-test.sy +================= begin: gcc-as ================= +================= begin: run ================= +[12:03:14] Pass 00-multi-add : ['TOTAL: 0H-0M-0S-0us\n'] + Pass 01-repeat-add : ['TOTAL: 0H-0M-0S-0us\n'] + Pass 02-distribute-imul : ['TOTAL: 0H-0M-0S-0us\n'] + Pass 03-addmulconst-test : ['TOTAL: 0H-0M-0S-0us\n'] ``` -此即为所有代码均编译错误, 因为在 `asm-handmade` 文件夹内的程序均期望返回 0. 自动判断与比对输出将在日后支持. - ## 更多文档 对于代码本身的高层次说明详见 [docs](docs/) 文件夹内的文档. 对于代码细节的说明详见代码注释. - 代码风格: - IR 设计: -- \ No newline at end of file +- IR 优化设计: +- 后端设计: \ No newline at end of file diff --git a/docs/backend.md b/docs/backend.md index 2312e36..a6b0c4b 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -1,13 +1,15 @@ # 后端架构简介 -对于后端而言,主要分为了三个部分: +对于后端而言,主要分为了四个部分: IR --> LIR (Low level IR) -LIR -- > ArmCode - 寄存器分配 +后端优化 + +LIR -- > ArmCode + ## 继承体系与类别总览 LIR的指令和操作数体系如下: @@ -19,7 +21,7 @@ ArmFunction > Unary : INeg, FNeg, > Binary - : IAdd, ISub, IRsb, IMul, IDiv, FAdd, FSub, FMul, FDiv, + : IAdd, ISub, IRsb, IMul, IDiv, ILMul, FAdd, FSub, FMul, FDiv, Bic > Terney : IMulAdd, IMulSub, FMulAdd, FMulSub, > FloatToInt @@ -31,6 +33,8 @@ ArmFunction > Store > Branch > Cmp + > StackOp + : ParamLoad, StackAddr, StackLoad, StackStore Operand > Addr @@ -46,11 +50,16 @@ Operand 对于ArmInst均是参考Arm指令设计而来, 其都共有着 cond: Any, Ge, Gt, Eq, Ne, Le, Lt -> 表示改指令在什么情况下执行(目前只有Branch跳转指令和MOV指令涉及) +> 表示改指令在什么情况下执行(目前只有Branch跳转指令、MOV指令和BranchToCond优化涉及) + +> (对于比赛用例的中, 出现了浮点数Nan的比较, 因而应该将Le改用Ls,Lt改用Mi, 目前本项目还是保留为Le, Lt) operands:表示该指令所拥有的操作数 > 并不是所有的指令都拥有操作数 比如Return和Branch指令 每个指令所拥有的操作数个数也不一定相同,比如Binary就有三个操作数 而Ternay则有四个操作数 +Shift: Arm汇编中的第2个操作数 +> 表示的是对于,用于在计算时实现移位操作 + ### 指令接口 对于指令的构造函数而言,如果第一次参数时ArmBlock则表示将该指令自动插入到该ArmBlock的尾部 @@ -90,19 +99,6 @@ replaceUseOperand(Operand oldOp, Operand op) 因为替换是把所有同类型的操作数都进行替换(可能会出现指令ADD VR1 VR2 VR2,为相同的虚拟寄存器的情况),因此如果不希望全部替换则可以只替换使用操作数的部分和定值操作数的部分(理论上对于使用操作数为同一个操作数时,同时替换肯定是最优解?) -对于寄存器分配可能会用到的指令为 ArmInstLoad 和 ArmInstStroe -```java -ArmInstLoad(Operand dst, Operand addr, Operand offset) -``` -将地址[addr, offset]中的值存入寄存器drc中 - -```java -ArmInstStroe(Operand src, Operand addr, Operand offset) -``` -将寄存器src的值存入[addr, offset] - -对于寄存器分配往往addr为寄存器SP 获得offset的方法可以参考栈结构解释当中的内容 - ### 寄存器接口 对于创建一个物理寄存器 可以直接通过 @@ -115,7 +111,7 @@ new IPhyReg("r0"), new IPhyReg("sp"), new IPhyReg(0), new IPhyReg(13) 通用寄存器: r0-r12 r14(lr) -向量寄存器: s0-s7 (其实存在s0-s31, 但其余寄存器的使用可能会出现向量化指令, 早期先只用s0-s7) +向量寄存器: s0-s31 IVirtualReg 应该分配通用寄存器 FVirtualReg 应该分配向量寄存器 @@ -130,7 +126,7 @@ Set getInstSet() ### 栈结构 -对于在 Linux ARM 上,栈向低地址方向增长 +对于在 Linux ARM 上, 栈向低地址方向增长, 高地址是栈顶 对于从IR -> LIR的过程中的Alloc称为CAlloc 而寄存器分配过程中的Alloc称为Alloc @@ -151,31 +147,31 @@ Set getInstSet() │ Parameter1 4 │ ├────────────────────────────┤ │ │ -│ AllocY 4 │ -├────────────────────────────┤ │ │ -│ ...... │ -├────────────────────────────┤ │ │ -│ Alloc2 4 │ -├────────────────────────────┤ +│ CAllocZ Nz │ +│ │ │ │ -│ Alloc1 4 │ ├────────────────────────────┤ │ │ +│ ...... │ │ │ +├────────────────────────────┤ │ │ -│ CAllocZ Nz │ +│ CAlloc1 N1 │ │ │ +├────────────────────────────┤ │ │ +│ AllocY 4 │ ├────────────────────────────┤ │ │ │ ...... │ -│ │ ├────────────────────────────┤ │ │ -│ CAlloc1 N1 │ +│ Alloc2 4 │ +├────────────────────────────┤ │ │ +│ Alloc1 4 │ └────────────────────────────┘ ``` @@ -193,39 +189,131 @@ Set getInstSet() ├────────────────────────────┤ │ │ ┌────────────┐ │ Parameter1 4 │ - │ ├────────┐ ├────────────────────────────┤ - │ Alloc1 │ │ │ │ - └─────┬──────┘ ┌────┼───────►│ AllocY 4 │ - ...│... │ │ ├────────────────────────────┤ - ▼ │ │ │ │ - ┌────────────┐ │ │ │ ...... │ - │ │ │ │ ├────────────────────────────┤ - │ CAlloc 1 ├───┼──┐ │ │ │ - └─────┬──────┘ │ │ │ ┌────►│ Alloc2 4 │ - ...│... │ │ │ │ ├────────────────────────────┤ - ▼ │ │ │ │ │ │ - ┌────────────┐ │ │ └──┼────►│ Alloc1 4 │ - │ │ │ │ │ ├────────────────────────────┤ - │ Alloc2 ├───┼──┼────┘ │ │ - └─────┬──────┘ │ │ │ │ - ...│... │ │ │ │ - ▼ │ │ ┌────►│ CAllocZ Nz │ - ┌────────────┐ │ │ │ │ │ - │ │ │ │ │ │ │ - │ CAlloc Z ├───┼──┼────┘ ├────────────────────────────┤ - └─────┬──────┘ │ │ │ │ - ...│... │ │ │ ...... │ - ▼ │ │ │ │ - ┌────────────┐ │ │ ├────────────────────────────┤ - │ │ │ │ │ │ - │ AllocY ├───┘ └─────────►│ CAlloc1 N1 │ - └────────────┘ │ │ + │ ├─────────┐ ├────────────────────────────┤ + │ Alloc1 │ │ │ │ + └─────┬──────┘ │ │ │ + ...│... │ │ │ + ▼ ┌─────┼──────►│ CAllocZ Nz │ + ┌────────────┐ │ │ │ │ + │ │ │ │ │ │ + │ CAlloc 1 ├───┼──┐ │ ├────────────────────────────┤ + └─────┬──────┘ │ │ │ │ │ + ...│... │ │ │ │ ...... │ + ▼ │ │ │ │ │ + ┌────────────┐ │ │ │ ├────────────────────────────┤ + │ | | └──┼──────►│ │ + │ Alloc2 ├───┼─┐ │ │ CAlloc1 N1 │ + └─────┬──────┘ │ │ │ │ │ + ...│... │ │ │ ├────────────────────────────┤ + ▼ │ │ ┌─┼──────►│ │ + ┌────────────┐ │ │ │ │ │ AllocY 4 │ + │ │ │ │ │ │ ├────────────────────────────┤ + │ CAlloc Z ├───┘ │ │ │ │ │ + └─────┬──────┘ │ │ │ │ ...... │ + ...│... │ │ │ │────────────────────────────┤ + ▼ │ │ │ │ │ + ┌────────────┐ └─┼─┼──────►│ Alloc2 4 │ + │ │ │ │ ├────────────────────────────┤ + │ AllocY ├───────┘ └──────►│ │ + └────────────┘ │ Alloc1 4 │ └────────────────────────────┘ ``` -对于一个函数最终的栈而言, 其最低层是CAlloc所分配的空间,再往上是寄存器分配所导致的Alloc, 再往上是该函数传入的在栈中的参数 +对于一个函数最终的栈而言, 其最低层是寄存器分配所导致的Alloc,再往上是CAlloc所分配的空间, 再往上是该函数传入的在栈中的参数,这个最终的栈结果和按照顺序生成的结果并不一样,因而按照处理顺序,是先获得了CAlloc的栈情况,在获得寄存器分配的Alloc的栈分配,但实际上为了更优秀的性能,因为访问寄存器分配的Alloc的栈分配情况更加的平凡,同时如果在栈上申请了一个较大空间,将寄存器分配的Alloc放在栈底也不用进行计算出偏移量再寻址(当偏移量超过+-4095(vfp为+-1020)时,无法直接通过Load Store进行偏移量寻址),同时也更好的支持申请不定长度数组的情况(只需要处理CAlloc和调用函数的情况,并不用处理寄存器分配时的偏移量,本项目并未实现) + +对于栈上的Load和Store等操作,均特化出了全新的类: + +> ParamLoad StackAddr StackLoad StackStore + +ParamLoad用于函数参数的Load,因为其栈修复的特殊性,因而单列 + +StackLoad,StackStore用于寄存器分配时的在栈上的Load和Store + +StackAddr用于表示一个基址或者一个CAlloc的在栈上的地址,当表示为一个基址时,它会捆绑多个ParamLoad,StackLoad,StackStore,从而保值所有的ParamLoad,StackLoad,StackStore都可以用`Ldr/Str 基址 相对偏移量`的方式来完成,因而不会出现多个Load和Store需要通过`ADD 地址 SP 偏移量` + `Ldr/Str 地址`的方式完成操作。实际上StackAddr为`ADD 基址 SP 偏移量`,其中保证了偏移量为n * 1024,这样保证了只有当`n > 2^8`才需要额外的MOVE操作,这里和Arm立即数的表示关系相关。 + +这部分和栈修复有关,通过上述优化后栈修复逻辑相对复杂,因而抽象出上述的指令进行处理。 + +## 寄存器分配 + +目前本项目寄存器分配采用的是简单的图着色寄存器,相较于完整的图着色寄存器分配缺少了coalesce优化 + +因为栈修复相关的逻辑,因而不能在寄存器分配的时候就确定下来某个虚拟寄存器对应替换为某个物理寄存器,因而每次寄存器分配后返回一个map,为虚拟寄存器到物理寄存器的映射 + +在本项目中有一些独特的优化,比如对于立即数和基址,可以不压入栈中,直接重新使用一个MOVE/ADD指令替换Load指令,同时尝试将局部(一个基本块内)的spill的同一个寄存器重新恢复为一个虚拟寄存器,再进行重新寄存器分配 + +在寄存器分配时对于选择spill节点的方式有待优化,本项目试过考虑引入循环深度、使用次数等参数,但并不能找到一个很好的拟合函数表示权值,另外参考其他项目可以考虑采用机器/深度学习,马尔科夫链等内容实现 + +## 后端优化 + +### 立即数乘除模 + +本项目当中添加了所有情况下的立即数除模优化,但除了对于`2^n`的特殊情况下有实际效果,其余效果并不明显 + +对于立即数乘法,只考虑对于下述情况进行优化: + +$$\mid x \mid = 2^x \pm 2^y / 0 \quad (x \geq y)$$ + +### 窥孔优化 + +目前采用的策略包括: + +``` +> 删除基本块相邻的直接跳转 +> 调整有条件跳转的方向 +> 替换LOAD/STORE寻址方式 +> 删除无用的MOVE,LOAD,STORE等指令 +> 三操作数指令变为四操作数指令 +> 四则运算利用移位运算合并 +...... +``` + +上述优化可以总结为三个部分:跳转优化、删去无用指令、强度增强,但暂未有Load和Store的自增自减的强度增强(逻辑较为复杂,因正确性不好保证因而删除) + +### BranchToCond + +将一些小的基本块执行变为指令条件执行,这主要是优化如下场景 +```c +if ( cond ) { + do something simple +} +``` + +能够优化的条件如下: +``` +1. 三个基本块相连,且跳转指令条件成功从第一个基本块跳转至第二个基本块,否则跳转至第三个基本块 +2. 跳转条件不为无条件跳转 +3. 第二个基本块只有一个前驱 +4. 第二个基本块的前驱为第一个基本块 +5. 第二个基本块只有一个后驱 +6. 第二个基本块的后驱为第三个基本块 +7. 第二个基本块指令条数小于等于4条 +8. 第二个基本块指令中无比较,跳转,返回指令 +9. 第二个基本块无条件执行指令(全部为Any) +``` + +BranchToCond优化配合IR的块重排可以起到更好的效果 + +## 后端杂谈 + +对于后端而言,在竞赛当中主要的优化点就在于尽可能的减少Load\Store和跳转的情况 + +这是因为一个Load/Store往往需要3-4个周期(具体几个周期我也没查到),跳转指令的执行确实只需要1个周期,但跳转指令会打断Arm的流水线,而Arm是三级流水线(取指、译码、执行),也就是说一个跳转指令相当于3个指令周期(但理论上应该有分支预测?),而其余的大部分计算都是1个周期,因此主要优化在这个上面 + +但本项目相较于以上的优化,还着重优化了海量函数参数,大量变量计算的场景,但是本竞赛当中并没有出这样并不符合实际场景的用例(为什么不像ACM一样出极端用例) + +## Arm后端tips + +### 开启向量化 + +通过修改fpscr实现 + +必须使用vmrs指令将fpscr加载到通用寄存器中,使用vmsr指令对寄存器进行操作,并将其移回fpscr + +其通过设置len = 执行向量化的寄存器的个数,stride = 在vfp寄存器中执行向量化跳过的间隔 + +len的值存储在fpscr的第16至18位中, stride存储在fpscr的20至21位中, 其中因为0没有意义,因此000 = 1, ... , 111 = 8 -因此对于在IR -> LIR的过程中 只涉及了CAlloc 其工作原理是获得当前的栈偏移 offset = func.getStackSize() 其地址就为 [sp, offset], 然后增加当前的栈大小,其为分配的空间大小n: func.addStackSize(n) +### BSS段 -对于CAlloc指令所导致的栈分配都先放在栈底,然后再是寄存器分配中的栈分配,其原理和CAlloc是一致的,先获得栈偏移 offset = func.getStackSize() 其地址就为 [sp, offset], 然后增加当前的栈大小: func.addStackSize(4) +BSS段通常是指用来存放程序中未初始化的全局变量的一块内存区域,其只记录占用空间的大小,由操作系统来负责清零 -对于最终的CodeGen而言,每个函数先在最开始执行 SUB SP SP func.getStackSize() 在最后再加上 ADD SP SP func.getStackSize() 因此对于每个函数最顶端的参数的获取,实际上是需要做一个栈修复,只需要对于每个函数的startBlock中的Load指令的offset增加func.getStackSize() \ No newline at end of file +对于全0初始化的全局数组,如果使用DATA段则最后生成的可执行文件也会附带所占用这么大空间的0,导致生成的可执行文件巨大 \ No newline at end of file diff --git a/docs/build_tools.md b/docs/build_tools.md deleted file mode 100644 index 23876c1..0000000 --- a/docs/build_tools.md +++ /dev/null @@ -1 +0,0 @@ -# 构建工具开发过程与选择 \ No newline at end of file