编译原理笔记09:代码生成

Posted by LiYixian on Tuesday, December 12, 2023 | 阅读 | ,阅读约 3 分钟

Summary:

  • 基本块和流图
  • 代码生成基本算法
  • 寄存器分配
  • 窥孔优化

设计问题

  1. 输入
    • 中间表示形式
    • 符号表信息
    • 类型检查和转换已经完成
  2. 输出
    • 汇编语言
  3. 内存管理
    • Ch7 中讨论的内存分配策略
  4. 指令集选择
  5. 寄存器分配
    • allocation, assignment
    • regiser-pair
  6. 计算顺序
  7. 代码生成方法

寻址方式开销:绝对 M (1)/寄存器 R (0)/索引 c(R) (1)/间接寄存器 *R (0)、间接索引 *c(R) (1)
指令开销:MOV R0,R1(1)/ MOV R5,M (2)/ ADD #1,R3 (2)/ SUB 4(R0),*12(R1) (3)

基本块和流图

把三地址码划分成基本块:

  1. 确定入口语句 leader
    • 程序第一条语句
    • 跳转的目的语句
    • 跳转之后的第一条语句
  2. 每个 leader——下个 leader(或程序结尾)是一个基本块

基本块的变换可以提高代码质量

  • 保结构变换:公共子表达式删除、无用代码删除、重命名临时变量、语句交换位置

划分基本块之后,命名,根据跳转语句(如果没有,就按顺序)建立逻辑转移关系,得到流图

下次引用信息

对语句 x:=y op z,从后往前扫描基本块,确定 x,y,z 下次使用的位置(确定变量的生存期),决定寄存器能否释放

计算下次引用信息:

  • 记录每个变量的下次引用信息和活跃信息
  • 假定临时变量在基本块出口后非活跃
  • 非临时变量在出口后活跃
  • 若临时变量跨基本块,假定其活跃

如果两个临时名字活跃期不重叠,就可以分配相同地址:

代码生成基本算法

每条 LLVM IR 指令 -> 目标语言指令;计算结果尽量存在寄存器里,除非

  1. 需要用寄存器进行其他计算
  2. 下面的语句是函数调用、转移、基本块结束

寄存器描述符:保存当前每个寄存器的内容
地址描述符:保存名字的地址(寄存器、栈位置、内存地址)

代码生成基本算法:

  1. 为计算结果分配位置 L (getreg)
  2. 提取操作数的值:查询其位置(地址描述符)
  3. 生成计算指令
  4. 更新计算结果的地址描述符和位置 L 的寄存器描述符
  5. 若操作数不再被引用,从寄存器描述符删除
    • 如果是赋值语句 x:=y,且 y 不再被引用,则直接 MOV y,x
  6. 在基本块出口,需要将活跃名字值存入内存

getreg 算法,对 x:=y op z,为 x 分配位置 L,依次尝试:

  1. 占用 y 的寄存器
    • y 的寄存器不包含其他名字;y 不再活跃、不再被引用
  2. 寻找空寄存器
  3. 寄存器替换
    • 数组索引
    • 已用寄存器 R,其中变量引用位置最远/已经存在内存中
  4. 内存

e.g.

寄存器分配

变量在点 p 处活跃:变量在 p 处的值在 p 或 p 之后还会用到
变量编号最小和最大的两个活跃点即其活跃区间

线性扫描寄存器分配(linear scan register allocation)算法:遍历每个活跃区间,分配物理寄存器
如果寄存器已满,则需要溢出到栈中,然后在操作该临时变量时插入对应的 load/store 指令(定义虚拟寄存器后 store 进内存里,使用前 load 其值),将该临时变量的活跃区间进行切分,以便重新进行寄存器分配。

窥孔优化

窥孔 peephole:一个滑动窗口
对窗口中的指令序列进行分析,如果存在性能更好的等价的指令序列,就替换

  1. 冗余 load/store 指令
  2. 不可达代码
  3. 控制流优化
  4. 代数优化
  5. 强度削弱:乘方变乘法,乘法变加法

需要得到更全局的信息,和数据流分析相结合;
回退滑动窗口/多次扫描