编译原理--04 符号表、运行时存储组织和代码优化复习(清华大学出版社第3版)
前言
目录 |
---|
01 文法和语言、词法分析复习 |
02 自顶向下、自底向上的LR分析复习 |
03 语法制导翻译和中间代码生成复习 |
04 符号表、运行时存储组织和代码优化复习 |
05 用C++手撕PL/0 |
第8章 静态语义分析和中间代码生成(续)
符号表
符号表需要在编译期间用到,记录符号的具体信息。本部分只讨论PL/0符号表的建立。
PL/0符号表结构
PL/0的符号表包含5个信息:
- NAME,符号名
- KIND,符号类型
- LEVEL/VAL,层次/值。如果类型为CONSTANT,存放的是常量的值;如果类型为VARIABLE或PROCEDURE,存放所属分程序的层次,主程序的层次为0;在主程序中定义的内容层次为1;主程序内第一层分程序中定义的内容层次为2,以此类推。
- ADR,地址。如果为简单变量或常量,则记录的是该量在数据区所占单元的相对地址,用DX表示给本层局部变量分配的相对存储位置,每说明一个变量后DX加1;如果为过程,则存放该过程的分程序入口地址(需要返填)
- SIZE,大小。该过程内局部变量的个数,再加上过程活动记录的头3个单元(DL,SL,RA)(需要返填)
例如下面的程序:
const a = 35, b = 49;
var c, d, e;
procedure p;
var g;
对应的符号表为:
NAME | KIND | VAL/LEVEL | ADD | SIZE |
---|---|---|---|---|
a | CONSTANT | 35 | ||
b | CONSTANT | 49 | ||
c | VARIABLE | LEV | DX | |
d | VARIABLE | LEV | DX+1 | |
e | VARIABLE | LEV | DX+2 | |
p | PROCEDURE | LEV | p的入口地址 | 4 |
g | VARIABLE | LEV+1 | DX |
又例如下面的程序:
const a = 25;
var x, y;
procedure p;
var z;
begin
...
end;
procedure r;
var x, s;
procedure t;
var v;
begin
...
end;
begin
...
end;
begin
...
end.
对应的符号表为:
NAME | KIND | VAL/LEVEL | ADD | SIZE |
---|---|---|---|---|
a | CONSTANT | 25 | ||
x | VARIABLE | LEV | DX | |
y | VARIABLE | LEV | DX+1 | |
p | PROCEDURE | LEV | p的入口地址 | 4 |
z | VARIABLE | LEV+1 | DX | |
r | PROCEDURE | LEV | r的入口地址 | 5 |
x | VARIABLE | LEV+1 | DX | |
s | VARIABLE | LEV+1 | DX+1 | |
t | PROCEDURE | LEV+1 | t的入口地址 | 4 |
v | VARIABLE | LEV+2 | DX |
第9章 运行时存储组织
PL/0程序运行栈中的过程活动记录
PL/0程序运行时,每一次过程调用都将在运行栈增加一个过程活动记录。 其中,当前活动记录的起始单元由基址寄存器b指出,结束单元是栈顶寄存器t所指单元的前一个单元。
PL/0的过程活动记录中的头3个单元是固定的联系信息:
- 静态链SL:存放的是定义该过程所对应的上一层过程,最近一次运行时的活动记录的起始单元。
- 动态链DL:存放的是调用该过程前正在运行过程的活动记录的起始单元。过程返回时当前活动记录要被撤销,此时需要动态链信息来修改基址寄存器b的内容。
- 返回地址RA:记录该过程返回后应该执行的下一条指令地址,即调用该过程的指令执行时指令地址寄存器p的内容加1
这样,每当一个过程被调用,就需要在栈上先分配3个空间用来存储上述信息,然后才是分配空间存储过程的局部变量。对于主过程,SL=DL=RA=0。
这里给出一道例题。对于下列程序:
var m, n, g:integer;
function gcd(m,n:integer):integer;
begin
if n = 0 then
g := m
else
g := gcd(n, m mod n)
end;
begin
m := 24;
n := 16;
g := gcd(m, n)
end.
它的运行栈为:
这一章可能要考的内容
- 运行栈的填写(静态链、动态链)
- display表(本质上是记录各个层定义的最新活动记录),建议自己看书
第10章 代码优化
优化技术简介
常用优化技术有:
- 删除多余运算
- 循环不变代码外提
- 强度削弱
- 变换循环控制条件
- 合并已知量
- 复写传播与删除无用赋值
删除多余运算
可以看到式做了和式重复的工作,可以改写成
循环不变代码外提
原代码:
块1
块2
可以看到式在每次循环都做重复的工作,可以把它提到循环外来,记得修改跳转:
块1
块2
强度削弱
把强度大的运算换成强度小的运算,比如用加法换乘法:
块1
块2
把式经过处理,并修改跳转:
块1
块2
变换循环控制条件
下面的代码中,和保持4倍的线性关系:
块1
块2
可以把循环条件改为,然后修改的初始赋值,这样在整个循环都没有被用上,可以剔除:
块1
块2
合并已知量
下面的代码中,在计算时,必定为1:
因此可以直接在编译期间算出它的值是4:
复写传播和删除无用赋值
看下面的代码:
块1
块2
四元式把的值写入中,但和的值在到之间没有发生改变,故将改为
此时式没有被引用,属于无用赋值,可以删掉。
然后,,对赋值,但也只是自我引用,其余地方没有需要用到,属于无用赋值,故可以删掉。
最终变为:
块1
块2
基本块、流图和循环
基本块
一个基本块内部是顺序执行的,故内部不能有任何停止、分支、跳转。
基本块的划分:
- 条件转移语句或者无条件转移语句和下一句语句之间要划分开
- 跳转的目标语句要和上一句语句之间划分开
例如:
经过划分后:
B1
/////////////////////////////////////////////////
B2
/////////////////////////////////////////////////
B3
/////////////////////////////////////////////////
B4
流图
流图 是在已经划分基本块的基础上,构造一个有向图。
- 两个相邻基本块如果上面的没有跳转,可以直接和下面的相连
- 如果当前基本块最后存在无条件跳转,直接和跳转的目标基本块相连
- 如果当前基本块存在最后有条件跳转,需要先和下面相邻的基本块相连,然后和跳转的目标基本块相连
上面的基本块集合为,可以用有向边集合,这里不画图。
循环
支配结点,指的是对任意两个结点m和n来说,如果从流图的首结点出发,到达n的任一通路都要经过m,则称m是n的支配结点,记为
下图是某个程序的流图,其结点即程序中的基本块
所有结点的支配结点集D(n):
该图的有向边集合为:
回边指的是存在一条边,使得。故上图的回边有
一个循环由其中的一条回边对应的两个结点,以及有通路到达而不经过的所有结点组成,并且保证是该循环的唯一入口结点。
如包含回边的循环为
包含回边的循环为
包含回边的循环为
这一章可能的考点
- 划分基本表、画出流图、求支配集、找回边、找循环
- 代码局部优化
PL/0编译程序
因为居然还有编程填空题这种恐怖存在,需要了解下面这些内容,不然填空都不知道怎么填。
可以用的全局变量如下:
全局变量 | 含义 |
---|---|
sym | 当前读取到的符号类型 |
num | 当前读取到的值 |
id | 当前读取到的标识符名称 |
cx | 当前中间代码将被写入时的索引 |
tx | 当前符号表将被写入的索引 |
code | 指令数组,类型为instruction |
在分析控制流的函数可以用的变量如下:
变量 | 含义 |
---|---|
cx1,cx2 | 分别记录条件为真/假时需要跳转的地址 |
instruction
的结构体如下:
typedef struct
{
int f; // 函数类别
int l; // 层级
int a; // 地址/立即数/操作类别
} instruction;
函数类别和操作类别如下:
enum opcode
{
LIT, // 取立即数
OPR, // 操作
LOD, // 读取
STO, // 保存
CAL, // 调用
INT, // 初始化空间
JMP, // 无条件跳转
JPC // 有条件跳转
};
enum oprcode
{
OPR_RET, OPR_NEG, OPR_ADD, OPR_MIN,
OPR_MUL, OPR_DIV, OPR_ODD, OPR_EQU,
OPR_NEQ, OPR_LES, OPR_LEQ, OPR_GTR,
OPR_GEQ
};
符号类别如下:
enum symtype
{
NUMBER,
// 符号类型
PLUS,
MINUS,
TIMES,
SLASH,
ODD,
EQU, // =
NEQ, // <>
LES, // <
LEQ, // <=
GTR, // >
GEQ, // >=
LPAREN, // (
RPAREN, // )
COMMA, // ,
SEMICOLON, // ;
PERIOD, // .
// 关键字
BEGINSYM,
ENDSYM,
IFSYM,
THENSYM,
WHILESYM,
DOSYM,
CALLSYM,
CONSTSYM,
VARSYM,
PROCEDURESYM
}
可以用的全局函数如下:
全局函数 | 含义 |
---|---|
getsym | 获取下一个符号的类型到sym。如果sym是number,则num将会存放值;如果sym是标识符,id将存放标识符名称 |
gen | 生成下一条指令,cx加1 |
处理while循环的题目
<while语句> ::= <while><条件><do><语句>
case WHILESYM:
__________; // 第一个空为cx1 = cx,记录JMP要跳转的位置
getsym();
condition(SymSetAdd(DOSYM, FSYS), LEV, TX);
__________; // 第二个空为cx2 = cx,记录JPC指令的位置
gen(JPC, 0, 0);
if (__________) // 第三个空为sym == DOSYM,处理到do
getsym();
else
error(18);
statement(fsys, lev, tx);
gen(__________); // 第四个空为jmp, 0, cx1,要知道跳转回开始判断条件的地方
__________; // 第五个空为code[cx2].a = cx,回填JPC指令的跳转位置
break;
处理if或if-else条件语句的题目
if条件语句图
if-else条件语句图
<条件语句> ::= <if><条件><then><语句>[<else><语句>]
下题处理了if条件语句和if-else条件语句的情况:
case IFSYM:
getsym();
condition(SymSetUnion(SymSetNew(THENSYM, DOSYM), FSYS), LEV, TX);
if (SYM == THENSYM)
getsym();
else
error(16);
________; // 第一个空为cx1 = cx;,记录JPC位置待回填
gen(JPC, 0, 0);
statement(SymSetUnion(SymSetNew(ELSESYM), FSYS), LEV, TX);
if (__________) // 第二个空为SYM != ELSESYM,此时在分析else符号
code[cx1].a = cx;
else
{
getsym();
cx2 = cx;
gen(JMP, 0, 0);
__________; // 第三个空为code[cx1].a = cx,此时在分析false部分的语句开头,回填JPC的地址
statement(FSYS, LEV, TX);
__________; // 第四个空为code[cx2].a = cx,此时为执行完true部分语句后的JMP回填跳转地址
}
break;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律