中间代码概述
一般编译程序都要生成中间代码,然后再生成目标代码。这样的主要优点是:可移植(与具体目标程序无关)、易于目标代码优化。生成中间代码的缺点是降低了编译效率,但瑕不掩瑜。
常用的中间代码形式有波兰表示、N-元表示、抽象语法树、抽象机代码等。
波兰表示
一般指后缀形式的波兰表达式。它能够不借助括号的帮助无二义性地表说明算术表达式。在编译领域,它也用来表达其他的语言结构,如条件转移等。作为中间代码,它的缺点是不方便优化。
算术表达式的波兰表示
这是数据结构课程中的知识。对于一个带有括号的中缀形式的四则运算算术表达式,可以利用一个符号栈将其转化为波兰表示。算法如下:
- 如果是数字,直接输出到波兰表示中;
- 如果是算术运算符(
+
,-
,*
,/
):- 如果符号栈为空或栈顶符号为左括号
(
,直接入栈; - 如果符号栈不为空且栈顶符号为算术运算符,比较与栈顶算术运算符的优先级:
- 如果栈顶运算符优先级较低,则将当前算术运算符入栈;
- 如果栈顶运算符优先级相同或较高,则将栈顶运算符出栈到波兰表示中,直到栈顶运算符优先级比当前运算符优先级低、或栈顶符号为左括号
(
、或栈为空;
- 如果符号栈为空或栈顶符号为左括号
- 如果是左括号
(
,直接入栈; - 如果是右括号
)
,说明栈中一定有左括号(
,将栈顶运算符出栈到波兰表示中,直到左括号成为栈顶(
,该左括号(
出栈但不入波兰表示,右括号)
也不入波兰表示。
例子:对于算术表达式
F
∗
3.1416
∗
R
∗
(
H
+
R
)
F*3.1416*R*(H+R)
F∗3.1416∗R∗(H+R)
其波兰表示为
F
3.1416
∗
R
∗
H
R
+
∗
F3.1416*R*HR+*
F3.1416∗R∗HR+∗
作为上述过程的逆过程,对于一个四则运算的波兰表示求其带括号的中缀形式的算术表达式的算法如下:
- 遇到操作数时,将其输出;
- 遇到操作符时,从栈中弹出相应数量的操作数进行运算,然后将运算结果推入栈中;
- 重复步骤1和步骤2,直到表达式结束;
- 最后栈中的唯一元素即为表达式的结果。
条件转移的波兰表示
- 定义单目运算
B
R
BR
BR(或
B
L
BL
BL),表示无条件跳转。则
g
o
t
o
<
l
a
b
e
l
>
goto <label>
goto<label>可以表示为中缀形式:
B R < l a b e l > BR<label> BR<label>
或后缀形式:
< l a b e l > B R <label>BR <label>BR - 定义二目运算
B
Z
BZ
BZ,表示条件为0则跳转。
则 i f n o t < e x p r > g o t o < l a b e l > if\space not<expr>goto<label> if not<expr>goto<label>可以表示为中缀形式
< e x p r > B Z < l a b e l > <expr>BZ<label> <expr>BZ<label>
或后缀形式:
< e x p r > < l a b e l > B Z <expr><label>BZ <expr><label>BZ
例子:
考虑稍复杂的条件转移:
i
f
<
e
x
p
r
>
t
h
e
n
<
s
t
m
t
1
>
e
l
s
e
<
s
t
m
t
2
>
if <expr>then<stmt_1>else<stmt_2>
if<expr>then<stmt1>else<stmt2>
定义
<
l
a
b
e
l
1
>
<label_1>
<label1>为
<
s
t
m
t
2
>
<stmt_2>
<stmt2>开头第一个字符,
<
l
a
b
e
l
2
>
<label_2>
<label2>为
<
s
t
m
t
2
>
<stmt_2>
<stmt2>后第一个字符,则它可以被表示为如下代码:
i
f
n
o
t
<
e
x
p
r
>
g
o
t
o
<
l
a
b
e
l
1
>
if\space not <expr> goto <label_1>
if not<expr>goto<label1>
<
s
t
m
t
1
>
<stmt_1>
<stmt1>
g
o
t
o
<
l
a
b
e
l
2
>
goto\space<label_2>
goto <label2>
<
s
t
m
t
2
>
<stmt_2>
<stmt2>
其中缀形式的波兰表示为:
<
e
x
p
r
>
B
Z
<
l
a
b
e
l
1
>
<expr>BZ<label_1>
<expr>BZ<label1>
<
s
t
m
t
1
>
<stmt_1>
<stmt1>
B
R
<
l
a
b
e
l
2
>
BR<label_2>
BR<label2>
<
s
t
m
t
2
>
<stmt_2>
<stmt2>
其后缀形式的波兰表示为:
<
e
x
p
r
>
<
l
a
b
e
l
1
>
B
Z
<
s
t
m
t
1
>
<
l
a
b
e
l
2
>
B
R
<
s
t
m
t
2
>
<expr><label_1>BZ<stmt_1><label_2>BR<stmt_2>
<expr><label1>BZ<stmt1><label2>BR<stmt2>
数组引用的波兰表示
对于
A
R
R
A
Y
[
<
I
N
D
E
X
1
>
]
[
<
I
N
D
E
X
2
>
]
.
.
.
[
<
I
N
D
E
X
n
>
]
ARRAY[<INDEX_1>][<INDEX_2>]...[<INDEX_n>]
ARRAY[<INDEX1>][<INDEX2>]...[<INDEXn>]
其后缀波兰表示为:
<
I
N
D
E
X
1
>
<
I
N
D
E
X
2
>
.
.
.
<
I
N
D
E
X
n
>
A
R
R
A
Y
A
R
R
A
Y
_
R
E
F
<INDEX_1><INDEX_2>...<INDEX_n>ARRAY\space ARRAY\_REF
<INDEX1><INDEX2>...<INDEXn>ARRAY ARRAY_REF
其中
A
R
R
A
Y
_
R
E
F
ARRAY\_REF
ARRAY_REF为
n
+
1
n+1
n+1目运算符。
N-元表示
又称三地址码。每条指令由 n n n个域组成,通常第一个域表示操作符,剩余 n − 1 n-1 n−1个数为操作数。常用的有三元式和四元式。
三元式
形式为:
<
o
p
e
r
a
t
o
r
>
,
<
o
p
e
r
a
n
d
1
>
,
<
o
p
e
r
a
n
d
2
>
<operator>,<operand_1>,<operand_2>
<operator>,<operand1>,<operand2>
如
x
+
y
x+y
x+y的三元式为
+
,
x
,
y
+,x,y
+,x,y。
但这样无法表示计算的结果。为了表示计算结果,可以在三元式前加上标号,使用计算结果时可以引用标号。
例1:
w*x+(y+z)
可表示为:
① *,w,x
② +,y,z
③ +,(1),(2)
例2:
if x > y then
z := x;
else z := y+1;
可以表示为:
① -,x,y
② BMZ,(1),(5)
③ :=,z,x
④ BR, ,(7)
⑤ +,y,1
⑥ :=,z,(5)
⑦ ...
其中,BMZ
为双目运算符,表示小于或等于零则跳转。
间接三元式
如果使用序号来引用计算结果,则计算顺序一有变动就要调整序号,不利于代码的优化。因此将计算顺序与计算结果进行解耦,抽离出一张操作表用于记录执行顺序,称为间接三元式。
例如:
A:=B+C*D/E
F:=C*D
可以表示为:
① *,C,D
② /,①,E
③ +,B,②
④ :=,A,③
⑤ :=,F,①
⑥ ...
操作表:①②③④①⑤
代码优化后的操作表可以是:①②③④⑤
四元式
形式为:
<
o
p
e
r
a
t
o
r
>
,
<
o
p
e
r
a
n
d
1
>
,
<
o
p
e
r
a
n
d
2
>
,
<
r
e
s
u
l
t
>
<operator>,<operand_1>,<operand_2>,<result>
<operator>,<operand1>,<operand2>,<result>
其结果通常是由编译引入的临时变量,可由编译程序分配一个寄存器或主存单元。
例子:
(
A
+
B
)
∗
(
C
+
D
)
−
E
( A + B ) * ( C + D ) - E
(A+B)∗(C+D)−E
的四元式中间代码为:
+,A,B,T1
+,C,D,T2
*,T1,T2,T3
-,T3,E,T4
*四元式特殊形式——SSA
SSA是Single Static Assignment(静态单一赋值)的缩写。其主要特征是每个变量只赋值一次。SSA形式的中间代码的优点是能够大大简化代码优化过程和获得更好的优化结果,因此将四元式写成SSA形式再进行优化是一种重要的优化手段。
例子:
有如下代码:
y := 1
...
y := 2
x := y + z
将其转化为SSA形式后:
y1 := 1
...
y2 := 2
x := y2 + z
计算机就能方便地判断y1是多余的变量,从而优化掉它。
在涉及分支时,可以采用以下方式将四元式转化为SSA形式:
原四元式流图:
重命名变量:
加入
Φ
\Phi
Φ节点