代码生成
以编译器前端生成的中间表示和相关的符号表信息作为输入,输出语义等价的目标程序
代码生成器有三个主要任务:
指令选择,寄存器分配和指派,指令排序
代码生成器设计中的问题
代码生成器的输入
中间表示形式,符号表
IR的中间表示形式的选择有很多,四元式,三元式,间接三元式等三地址表示方式.
也包括诸如字节代码和堆栈机代码的虚拟机表示方式.
后缀表示的线性表示方式;
语法树和DAG的图形表示方式;
目标程序
RISC机通常有很多寄存器,三地址指令,简单的寻址方式和一个相对简单的指令集体系结构.
CISC机通常有较少寄存器,两地址指令,多种寻址方式,多种类型的寄存器,可变长度指令,具有副作用的指令.
输出一个使用绝对地址的机器语言程序的优点是程序可放在内存某个固定位置,立即执行.编译和执行快.
输出可重定位机器语言程序可使各个程序能被分别编译.一组可重定位的目标模块可被一个链接加载器链接到一起并加载运行.
可重定位需要为链接和加载付出代价.优势是获得灵活性.
如目标机没有自动处理重定位,编译器就需向加载器提供明确的重定位信息.
指令选择
代码生成器需把IR程序映射成为可以在目标机上运行的代码序列.
完成映射的复杂性由如下因素决定:
1.IR层次
2.指令集体系结构本身特性
3.要达到代码质量
如每个形如x=y+z的三地址语句,可被翻译为如下
LD R0, y
ADD R0, R0, z
ST x, R0
大多数机器上,一个给定的IR程序可用很多种不同的代码序列来实现.
如目标机有一个 加一 指令,则a=a+1可用一个指令INC a实现
寄存器分配
寄存器的使用常被分解为两个子问题:
1.寄存器的分配
2.寄存器的指派
求值顺序
目标语言
一个简单的目标机模型
我们的目标计算机是一个三地址机器的模型
具有加载和保存操作,计算操作,跳转操作,条件跳转
这个计算机的内存按字节寻址,具有n个通用寄存器R0,...,Rn-1.
一个完成的汇编语言有几十到上百个指令.
我们使用一个有限的指令集合.
假设所有运算分量都是整数.
大部分指令含一个运算符,然后是一个目标地址,最后是一个源运算分量列表.
指令之前可能有一个标号.
假设有如下种类的指令可用
1.加载运算
指令LD dst, addr把位置addr上的值加载到位置dst.
表示dst = addr.
最常见形式是LD r, x
把位置x中的值加载到寄存器r中.
形如LD r1, r2的指令是一个寄存器到寄存器的拷贝运算.
把寄存器r2的内容拷贝到寄存器r1
2.保存运算
ST x, r把寄存器r中的值保存到位置x.
表示x = r
3.计算运算
OP dst, src1, src2
dst = src1 op src2
单目运算符没src2
4.无条件跳转
BR L使得控制流转向标号为L的机器指令
5.条件跳转
Bcond r, L
r是一个寄存器,L是一个标号
cond代表了对寄存器r中的值所做的某个常见测试
假设目标机具有多种寻址模式
1.在指令中,一个位置可是一个变量名x,它指向分配给x的内存位置
2.一个位置也可是一个带有下标的形如a(r)的地址,其中a是一个变量,r是一个寄存器.a(r)表示的内存位置按如下方式计算得到:a的左值加上存放寄存器r中的值.
如指令LD R1, a(R2)的效果是R1 = contents(a + contents(R2)),其中contents(x)表示x所代表的寄存器或内存位置中存放的内容.
3.一个内存位置可是一个以寄存器作为下标的整数.
如LD R1, 100(R2)的效果就是使得R1 = contents(100+contents(R2))
也即首先计算寄存器R2的值加上100得到的和
然后把这个和所指向的位置中的值加载到R1中
4.还支持另外两种间接寻址模式
*r表示寄存器r的内容所代表的位置上存放的内存位置
*100(r)表示在r中内容加上100的和所代表的位置上的内容所代表的位置
如LD R1, *100(R2)效果是把R1设置为contents(contents(100+contents(R2)))
5.最后,支持一个直接常数寻址模式.
在常数面前有一个前缀#.
指令LD R1, #100把整数100加载到R1中.
而ADD R1, R1, #100则把100加到寄存器R1中.
程序和指令的代价
目标代码中的地址
将说明如何用静态和栈式内存分配为简单的过程调用和返回生成代码,以此将IR中的名字转换成为目标代码中的地址.
这个空间被划分成为四个代码及数据区域
1.一个静态确定的代码区Code.
这个区存放可执行的目标代码.
目标代码大小可在编译时确定
2.一个静态确定的静态数据区Static.
这个区存放全局常量和编译器生成的其他数据.
全局常量和编译器数据的大小也可在编译时刻确定
3.一个动态管理的堆区Heap.
这个区存放程序运行时刻分配和释放的数据对象.
4.一个动态管理的栈区Stack.
静态分配
关注下面的三地址语句
call callee
return
halt
action
先说明如何在过程调用时在一个活动记录中存放返回地址,及如何在过程调用结束把控制返回到这个地址.
假设活动记录的第一个位置存放返回地址.
ST callee.staticArea, #here+20
BR callee.codeArea
ST指令把返回地址保存到callee的活动记录的开始处
BR把控制传递到被调用过程callee的目标代码上
BR *callee.staticAction
栈分配
名字的运行时刻地址
基本块和流图
本节介绍一种用图来表示中间代码的方法
这个表示方法可按照如下方法构造
1.把中间代码划分为基本块
每个基本块是满足下列条件的最大的连续三地址指令序列
1.1.控制流只能从基本块中的第一个指令进入该块
即没跳转到基本块中间的转移指令
1.2.除了基本块的最后一个指令,控制流在离开基本块之前不会停机或跳转
2.基本块形成了流图的结点
流图的边指明了哪些基本块可紧随一个基本块之后运行
基本块
把一个三地址指令序列分割成为基本块
以第一个指令作为一个新基本块的开始,
不断把后续指令加进去
直到碰到无条件跳转,条件跳转或下一个指令前面的标号为止
// 把三地址指令序列划分成为基本块
输入:一个三地址指令序列
输出:输入序列对应的一个基本块列表,其中每个指令恰好被分配给一个基本块
方法:
确定中间代码序列中哪些指令是首指令
即某个基本块的第一个指令
跟在中间程序末端之后的指令的不包含在首指令集合中.
选择首指令的规则如下:
1.中间代码的第一个三地址指令是一个首指令
2.任意一个条件或无条件转移指令的目标指令是一个首指令
3.紧跟在一个条件或无条件转移指令之后的指令是一个首指令
每个首指令对应的基本块包括了从它自己开始,直到下一个首指令[不含]或中间程序的结尾指令间的所有指令.
每个首指令对应的基本块包括了从它开始直到下一个首指令之前的所有指令
后续使用信息
一个三地址语句中对一个名字的使用的定义如下.
假设三地址语句i给x赋了一个值.
如语句j的一个运算分量为x,且从语句i开始可通过未对x进行赋值的路径到达语句j,
则说语句j使用了在语句i处计算得到的x的值.
可进一步说x在语句i处活跃.
用来确定活跃性和后续使用信息的算法对每个基本块进行一次反向的遍历
把得到的信息存放到符号表
假设每个过程调用的指令是一个新的基本块的开始
// 对一个基本块中的每一个语句确定活跃性与后续使用信息
输入:一个三地址语句的基本块B,假设开始时候符号表显示B中的所有非临时变量都是活跃的.
输出:对B的每一个语句i:x=y+z,将x,y及z的活跃性信息及后续使用信息关联到i
方法:
从B的最后一个语句开始,反向扫描到B的开始处
对每个语句i:x=y+z
做下面的处理:
1.把在符号表中找到的有关x,y和z的当前后续使用和活跃性信息与语句i关联起来
2.在符号表中,设置x为不活跃和无后续使用
3.在符号表中,设置y和z为活跃,并把它们的下一次使用设置为语句i
流图
当将一个中间代码程序划分为基本块后,
用一个流图来表示它们之间的控制流
流图的结点是这些基本块.
从基本块B到基本块C间有一条边当且仅当基本块C的第一个指令可能紧跟在B的最后一个指令之后.存在这样一条边原因:
1.有一个从B的结尾跳到C的开头的跳转语句
2.按原来三地址语句序列的顺序,C紧跟在B后,B的结尾无跳转
从入口到流图的第一个可执行结点有一条边
从任何包含了可能是程序的最后执行指令的基本块到出口有一条边
如程序的最后指令不是无条件转移,则包含了程序最后一条指令的基本块是出口结点的前驱.任何包含了跳到程序外的跳转指令的基本块也是出口结点的前驱.
流图的表示方式
循环
如下列条件成立,我们就说流图中的一个结点集合L是一个循环
1.在L中有一个被称为循环入口的结点
它是唯一的其前驱可能在L之外的结点.
也即从整个流图的入口结点开始到L中的任何结点的路径都必然经过循环入口结点
且这个循环入口结点不是整个流图的入口结点本身
2.L中的每个结点都有一个到达L的入口结点的非空路径,且该路径全部在L中
基本块的优化
基本块的DAG表示
把一个基本块转换为一个DAG
按如下方式为一个基本块构造DAG
1.基本块中出现的每个变量都有一个对应的DAG结点表示其初始值
2.基本块中的每个语句s都有一个相关的结点N.N的子结点是基本块中的其他语句的对应结点.这些语句在s之前,最后一个对s所使用的某个运算分量进行定值的语句.
3.结点N的标号是s中的运算符;同时还有一组变量被关联到N,表示s是在此基本块内最晚对这些变量进行定值的语句
4.某些结点被指明为输出结点.这些结点的变量在基本块的出口处活跃.
这些变量的值可能以后会在流图的另一个基本块被用到.
基本块的DAG表示使我们可对基本块所代表的代码进行一些转换,以改进代码质量
1.可消除局部公共子表达式
公共子表达式就是重复计算一个已经计算得到的值的指令
2.可消除死代码,即计算得到的值不会被使用的指令
3.可对相互独立的语句重新排序,重排序可降低一个临时值需保持在寄存器中的时间
4.可用代数规则重新排列三地址指令的运算分量顺序
寻找局部公共子表达式
一个新结点M将被加入DAG中时,
检查是否存在一个结点N,它和M有同样的运算符和子结点,且子结点顺序相同.
如存在,N计算的值和M计算的值一样.可用N替换M.
消除死代码
在DAG上消除死代码操作可按如下方式实现
从一个DAG上删除所有没有附加活跃变量的根结点
重复应用,即可消除DAG中死代码对应结点
代数恒等式使用
1.代数恒等式
2.局部强度消减
3.常量合并
数组引用的表示
在DAG中表示数组访问的正确方法如下
1.从一个数组取值并赋给其他变量的运算[如x=a[i]]用一个新创建的运算符为=[]的结点表示.
这个结点的左右子结点分别代表数组初始值和下标i.
变量x是这个结点的标号之一.
2.对数组的赋值[如a[j]=y]用一个新创建的运算符[]=结点来表示.这个结点的三个子结点分别表示a0,j和y.没有变量用这个结点标号.
此结点的创建杀死了所有当前已经建立的,其值依赖于a0的结点.一个被杀死的结点不能在获得任何标号,不可能成为一个公共子表达式.
指针赋值和过程调用
运算符=*需把当前所有带有附加标识符的结点当做其参数.
这么做会影响死代码消除过程.
*=运算符会把迄今构造出的DAG中的其他结点全部杀死
从DAG到基本块的重组
一个简单的代码生成器
寄存器和地址描述符
代码生成算法
运算的机器指令
复制语句的机器指令
基本块的收尾处理
管理寄存器和地址描述符