代码生成器的主要任务
一、指令选择
- 选择适当的目标机指令来实现中间表示(IR)语句,
- 例如:三地址语句 x = y + z
目标代码: LD R0, y /* 把y的值加载到寄存器R0中*/ ADD R0, R0, z /* z加到R0上*/ ST x, R0 /* 把R0的值保存到x中*/
- 如上,直接进行生成,会产生一些冗余指令。目标代码需要进一步优化。
二、寄存器分配和指派:
- 把哪个值放在哪个寄存器中
三、指令排序:
- 按照什么顺序来安排指令的执行
一个简单的目标机模型
三地址机器模型:
- 加载、保存、运算、跳转等操作
- 内存按字节寻址
- n个通用寄存器 R 0 , R 1 , … , R n − 1 R_0, R_1, …, R_{n-1} R0,R1,…,Rn−1
- 假设所有的运算分量都是整数
- 指令之间可能有一个标号
目标机器的主要指令
-
加载指令 LD dst, addr
- LD r, x
- LD r1, r2
-
保存指令 ST x, r
-
运算指令 OP dst, src1, src2
-
无条件跳转指令 BR L
-
条件跳转指令 Bcond r, L
- 例: BLTZ r, L
寻址模式
-
变量名a
- 例如:LD R1, a,即R1 = contents(a),将地址 a 中的内容放到寄存器 R1
-
a(r),a是一个变量,r是一个寄存器
- 例如:LD R1, a(R2),即R1 = contents(a + contents(R2))
-
c(r),c是一个整数
- 例如:LD R1, 100(R2),即R1 = contents(contents(R2) + 100),表示寄存器 R2 中存放的地址,加上整数c,其表示的地址所存放的内容。
-
∗r,在寄存器r的内容所表示的位置上存放的内存位置
- 例如:LD R1, *R2,即R1 = contents(contents(contents(R2))),这是间接寻址模式
-
∗c(r),在寄存器r中内容加上c后所表示的位置上存放的内存位置
- 例如:LD R1, *100(R2),即R1 = contents(contents(contents(R2) + 100))
-
KaTeX parse error: Expected ‘EOF’, got ‘#’ at position 1: #̲c,
- 例如LD R1, #100,即R1 = 100。
指令选择
运算语句的目标代码
-
三地址语句:x = y - z
-
目标代码:
LD R1 , y // R1 = y LD R2 , z // R2 = z SUB R1 , R1 , R2 // R1 = R1 - R2 ST x , R1 // x = R1
-
尽可能避免使用上面的全部四个指令,如果:
- 所需的运算分量已经在寄存器中了
- 运算结果不需要存放回内存
数组寻址语句的目标代码
- 三地址语句:b = a[ i ](a是一个实数数组,每个实数占8个字节)
- 目标代码:
LD R1 , i // R1 = i MUL R1 , R1, 8 // R1=R1 * 8 LD R2 , a(R1) // R2=contents ( a + contents(R1) ) ST b , R2 // b = R2
- 三地址语句:a [ j ] = c(a是一个实数数组,每个实数占8个字节)
- 目标代码:
LD R1 , c // R1 = c LD R2 , j // R2 = j MUL R2 , R2 , 8 //R2 = R2 * 8 ST a(R2) , R1 // contents(a+contents(R2))=R1
指针存取语句的目标代码
- 三地址语句:x = *p
- 目标代码:
LD R1, p // R1 = p LD R2, 0 (R1) // R2 = contents ( 0 + contents (R1) ) ST x , R2 // x = R2
- 三地址语句:*p = y
- 目标代码:
LD R1 , p // R1 = p LD R2 , y // R2 = y ST 0(R1), R2 //contents ( 0 + contents ( R1 ) ) = R2
条件跳转语句的目标代码
- 三地址语句:if x < y goto L
- 目标代码:
LD R1 , x // R1 = x LD R2 , y // R2 = y SUB R1 , R1 , R2 // R1=R1 - R2 BLTZ R1 , M // if R1 < 0 jump to M
- M是标号为L的三地址指令所产生的目标代码中的第一个指令的标号。
过程调用和返回的目标代码
静态区存储分配
- 方法调用
- 三地址语句:call callee
- 目标代码
ST callee.staticArea, #here + 20 //即callee的活动记录在静态区中的起始位置 BR callee.codeArea, //即calle的目标代码在代码区中的起始位置
- 方法返回
- 三地址语句:return
- 目标代码
BR * callee.staticArea // 间址,返回地址在 callee 栈帧底部
栈式存储分配
- 方法调用
- 三地址语句:call callee
- 目标代码
ADD SP, SP, #caller.recordsize ST 0(SP), #here+16 // 把返回地址压到被调过程的栈帧底部 BR callee. code Area
- 方法返回
- 三地址语句:return
- 目标代码
被调用过程:BR* 0(SP) 调用过程:SUB SP, SP, #caller.recordsixe
寄存器的选择
三地址语句的目标代码生成
对每个形如x = y op z的三地址指令I,执行如下动作:
- 调用函数 getreg(I)来为x、y、z选择寄存器,把这些寄存器称为 R x R y R z R_x R_y R_z RxRyRz
- 如果 R y R_y Ry 中存放的不是y ,则生成指令“LD Ry , y′ ′”。y′ 是存放y的内存位置之一
- 类似的,如果Rz中存放的不是z,生成指令“LD Rz , z ′ ”
- 生成目标指令“OP Rx , Ry , Rz ”
寄存器描述符和地址描述符
寄存器描述符(register descriptor):
- 记录每个寄存器当前存放的是哪些变量的值
地址描述符(address descriptor):
- 记录运行时每个名字的当前值存放在哪个或哪些位置
- 该位置可能是寄存器、栈单元、内存地址或者是它们的某个集合
- 这些信息可以存放在该变量名对应的符号表条目中
基本块的收尾处理
对于一个在基本块的出口处可能活跃的变量x
- 如果它的地址描述符表明它的值没有存放在x的内存位置上,则生成指令“ST x, R” ( R是在基本块结尾处存放x值的寄存器)
管理寄存器和地址描述符
对于指令LDR, x":
- 修改R的寄存器描述符,使之只包含x
- 修改x的地址描述符,把R作为新增位置加入到x的位置集合中
- 从任何不同于x的地址描述符中删除R
对于指令"OP Rx, Ry ,Rz,":
- 修改Rx的寄存器描述符,使之只跑含x
- 从任何不同于Rx的寄存器描述符中删除x
- 修改x的地址描述符,使之只包含位置Rx
- 从任何不同于x的地址描述符中删除Rx
对于指令“ST x, R”:
- 修改x的地址描述符,使之包含自己的内存位置
对于复制语句x=y,如果需要生成加载指令"LD Ry, y’"则:
- 修改Ry的寄存器描述符,使之只包含y
- 修改y的地址描述符,把Ry作为新增位置加入到y的位置集合中
- 从任何不同于y的变量的地址描述符中删除Ry
- 修改Ry的寄存器描述符,使之也包含x
- 修改x的地址描述符,使之只包含Ry
窥孔优化
-
窥孔(peephole)是程序上的一个小的滑动窗口
-
窥孔优化是指在优化的时候,检查目标指令的一个滑动窗口(即窥孔) ,并且只要有可能就在窥孔内用更快或更短的指令来替换窗口中的指令序列。
-
也可以在中间代码生成之后直接应用窥孔优化来提高中间表示形式的质量。
具有窥孔优化特点的程序变换的例子
- 冗余指令删除
- 例如:消除冗余的加载和保存指令
- 例如:消除不可达代码,一个紧跟在无条件跳转之后的不带标号的指令可以被删除。
- 控制流优化
- 在代码中出现跳转到跳转指令的指令时,某些条件下可以使用一个跳转指令来代替。
- 如果不再有跳转到L1的指令,并且语句L1: goto L2之前是一个无条件跳转指令,则可以删除该语句。
- 代数优化
- 代数恒等式:消除窥孔中类似于x=x+0或x=x*1的运算指令。
- 强度削弱:
对于乘数(除数)是2的幂的定点数乘法(除法) ,用移位运算实现代价比较低;
除数为常量的浮点数除法可以通过乘数为该常量倒数的乘法来求近似值。
- 机器特有指令的使用
- 充分利用目标系统的某些高效的特殊指令来提高代码效率。
- 例如:INC指令可以用来替代加1的操作。