编译器竞赛总结

Table of Contents

这学期参加了编译系统设计赛,受益匪浅。

在比赛的初期,我拖延症十分严重,一方面是我之前从来没写过 C++,二是我对基于 SSA 形式的编译器不算非常了解,处于摸黑的状态,尤其是后端优化,这部分的内容在很多的编译原理教材里都被忽略了。现在看来,前期没有能重视一些重要的学习资源,导致整个项目充满着一排脑袋想出来的设计,之后意识到错误时已为时已晚,只能通过各种奇怪的 Hack 来修补,导致整个项目的代码异常 Bloat 且难懂

1. 反思

  • 前端:

    我们没有用手写递归下降的方法,而是使用 Bisonc++Flexc++ 直接生成;这部分算是整个项目里写的最清楚的部分,不过 SysY 这个语言从语法上并不算特别复杂(?),或许自己从头写 Parser 也是可行的?

    • 一些相关的参考资料: Parsing Technique
  • 中端:

    大部分队伍应该是基于 LLVM IR 来写的,这是一个比较讨巧的做法,因为这样不仅可以很方便地验证前端的正确性(只需要把 emit 出的 IR 文件扔给 clang 看能不能跑出正确的结果就行),同时在后续优化时也可以和 LLVM 的 Pass 做对比来查看有效性/正确性等,但我们在写基础框架时没有考虑到这一层,我们的 IR 是基于 shecc 这个编译器项目的 IR 修改而成,日后这个设计直接拖累了中端写 Pass 的效率,因为 shecc 是一个教学用编译器,它在开发时没有考虑过写复杂优化的情况(事实上,它的优化只有一些简单的窥孔),所以为了让它能够进行复杂优化,我们不得不整出很多额外的数据结构来传递这些信息(这其中又使用了大量的 STL 库,导致开不开 -O3 对我们编译器的性能影响很大,总之全是 bad practice)

    • 构建 SSA 的算法模仿 LLVM: 先用 Semi-NCA 计算支配树,然后用数据流方程求解活跃性,插 Phi-Node,重命名变量
    • 现在想想,应该直接用 LLVM IR 或者 Pizlonator SSA 的,这两个设计都是简洁且 robust 的设计,可惜那时候我不知道
  • 后端:

    最大的失误是没有在 SSA-form 下做寄存器分配,同时策略过于简单,导致在复杂优化的情况下会给出异常糟糕的分配结果(例如在循环展开后把循环内变量给 spill 到内存,导致多出一堆内存操作)

    • 在 SSA-form 下做寄存器分配有特殊优势:它构建出的冲突图具有特殊的性质,可以在多项式时间内找到解,同时,保留 SSA-form 也可以避免迭代式的解构,让整个寄存器分配变成线性的流程,对降低复杂度和代码量很有帮助
    • 总结回顾,寄存器分配其实是可以渐进完成的,例如一开始先全部 spill 到栈上,然后 codegen 时用六个寄存器(整数三个,浮点数三个,对应 dest/src0/src1)进行加载,这样首先能保证正确性,我们花费了太多时间在跑通最开始那个糟糕的线性扫描方法,导致后面来不及写新的,更好的分配方式了。
  • CI:

    写本地测试时,考虑了 QEMU 和 RISCV 硬件的两种评测方式,本来应该是很完善的框架,但最后却把我们坑了,因为主板方不知道那整来的 FPGA 板子的内存操作异常慢,Store 和 Load 的开销很大,但本地模拟器和市面上能买到的 RISCV 硬件不会有这种问题,导致了实际上最大的瓶颈在本地测试时不可见,直接影响是我们决赛时修了两天的中端 Pass,在距离截止时间4小时才发现问题出在内存操作,但此时修已经来不及了

2. 其它

  • 中端 Pass 中,GVN/SCCP 很重要,循环分析/规范化很重要,测试用例里有很多循环相关的用例,做了类似 LICM 的特定优化会有很大的提升;以及,由于开发板的内存操作很低效,最好有 SROA 这样的 Pass 把内存操作提升到寄存器赋值,实现后其它 Pass 也能有更多的优化机会,能快不少
  • 后端代码生成时,注意浮点数相关的操作,SysY 只有 32 位的 int 和 float,如果用了 64 位的 Add 操作,需要加一个 sext.w

Created: 2025-08-22 五 17:33

Validate