編譯原理--04 符號表、運行時存儲組織和代碼優化復習(清華大學出版社第3版)


前言

目錄
01 文法和語言、詞法分析復習
02 自頂向下、自底向上的LR分析復習
03 語法制導翻譯和中間代碼生成復習
04 符號表、運行時存儲組織和代碼優化復習
05 用C++手撕PL/0

第8章 靜態語義分析和中間代碼生成(續)

符號表

符號表需要在編譯期間用到,記錄符號的具體信息。本部分只討論PL/0符號表的建立。

PL/0符號表結構

PL/0的符號表包含5個信息:

  1. NAME符號名
  2. KIND符號類型
  3. LEVEL/VAL層次/值。如果類型為CONSTANT,存放的是常量的值;如果類型為VARIABLEPROCEDURE,存放所屬分程序的層次,主程序的層次為0;在主程序中定義的內容層次為1;主程序內第一層分程序中定義的內容層次為2,以此類推。
  4. ADR地址。如果為簡單變量或常量,則記錄的是該量在數據區所占單元的相對地址,用DX表示給本層局部變量分配的相對存儲位置,每說明一個變量后DX加1;如果為過程,則存放該過程的分程序入口地址(需要返填
  5. 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個單元是固定的聯系信息:

  1. 靜態鏈SL:存放的是定義該過程所對應的上一層過程,最近一次運行時的活動記錄的起始單元。
  2. 動態鏈DL:存放的是調用該過程前正在運行過程的活動記錄的起始單元。過程返回時當前活動記錄要被撤銷,此時需要動態鏈信息來修改基址寄存器b的內容。
  3. 返回地址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.

它的運行棧為:

這一章可能要考的內容

  1. 運行棧的填寫(靜態鏈、動態鏈)
  2. display表(本質上是記錄各個層定義的最新活動記錄),建議自己看書

第10章 代碼優化

優化技術簡介

常用優化技術有:

  1. 刪除多余運算
  2. 循環不變代碼外提
  3. 強度削弱
  4. 變換循環控制條件
  5. 合並已知量
  6. 復寫傳播與刪除無用賦值

刪除多余運算

\((1)T_1:=4*I\\ (2)T_2:=addr(A)-4\\ (3)T_3:=T_2[T_1]\\ (4)T_4:=4*I\\ (5)T_5:=addr(B)-4\\ (6)T_6:=T_5[T_4]\)

可以看到\((4)\)式做了和\((1)\)式重復的工作,可以改寫成\(T_4:=T_1\)

循環不變代碼外提

原代碼:
塊1
\((1)P:=0\\ (2)I:=1\)
塊2
\((3)T_1:=4*I\\ (4)T_2:=addr(A)-4\\ (5)T_3:=T_2[T_1]\\ (6)P:=P+T_3\\ (7)I:=I+1\\ (8)if \;I<=20\;goto\;(3)\)

可以看到\((4)\)式在每次循環都做重復的工作,可以把它提到循環外來,記得修改跳轉:
塊1
\((1)P:=0\\ (2)I:=1\\ (3)T_2:=addr(A)-4\)
塊2
\((4)T_1:=4*I\\ (5)T_3:=T_2[T_1]\\ (6)P:=P+T_3\\ (7)I:=I+1\\ (8)if \;I<=20\;goto\;(4)\)

強度削弱

把強度大的運算換成強度小的運算,比如用加法換乘法:
塊1
\((1)P:=0\\ (2)I:=1\\ (3)T_2:=addr(A)-4\)
塊2
\((4)T_1:=4*I\\ (5)T_3:=T_2[T_1]\\ (6)P:=P+T_3\\ (7)I:=I+1\\ (8)if \;I<=20\;goto\;(4)\)

\((4)\)式經過處理,並修改跳轉:
塊1
\((1)P:=0\\ (2)I:=1\\ (3)T_1:=0\\ (4)T_2:=addr(A)-4\)
塊2
\((5)T_1:=T_1+4\\ (6)T_3:=T_2[T_1]\\ (7)P:=P+T_3\\ (8)I:=I+1\\ (9)if \;I<=20\;goto\;(5)\)

變換循環控制條件

下面的代碼中,\(I\)\(T_1\)保持4倍的線性關系:
塊1
\((1)I:=1\\ (2)T_1:=4*I\)
塊2
\((3)P:=T_2[T_1]\\ (4)I:=I+1\\ (5)T_1=T_1+4\\ (6)if\;I<=20\;goto\;(3)\)

可以把循環條件\(I<=20\)改為\(T_1<=80\),然后修改\(T_1\)的初始賦值,這樣\(I\)在整個循環都沒有被用上,可以剔除:
塊1
\((1)T_1:=4\)
塊2
\((2)P:=T_2[T_1]\\ (3)T_1=T_1+4\\ (4)if\;T_1<=80\;goto\;(2)\)

合並已知量

下面的代碼中,在計算\(4*I\)時,\(I\)必定為1:
\((1)I:=1\\ (2)T_1:=4*I\)

因此可以直接在編譯期間算出它的值是4:
\((1)I:=1\\ (2)T_1:=4\)

復寫傳播和刪除無用賦值

看下面的代碼:
塊1
\((1)T_1:=4\\ (2)I:=1\)
塊2
\((3)T_2:=T_1\\ ...\\ (7)T_3:=T_4[T_2]\\ (8)T_1:=T_1+T_3\\ (9)I:=I+1\\ (10)if\;T_1<=80\;goto\;(3)\)

四元式\((3)\)\(T_1\)的值寫入\(T_2\)中,但\(T_2\)\(T_1\)的值在\((3)\)\((7)\)之間沒有發生改變,故將\((7)\)改為\(T_3:=T_4[T_1]\)

此時\((3)\)式沒有被引用,屬於無用賦值,可以刪掉。

然后,\((2)\),\((9)\)\(I\)賦值,但也只是自我引用,其余地方沒有需要用到\(I\),屬於無用賦值,故可以刪掉。

最終變為:
塊1
\((1)T_1:=4\)
塊2
\(...\\ (5)T_3:=T_4[T_1] (6)T_1:=T_1+T_3 (7)if\;T_1<=80\;goto\;(2)\)

基本塊、流圖和循環

基本塊

一個基本塊內部是順序執行的,故內部不能有任何停止、分支、跳轉。

基本塊的划分:

  1. 條件轉移語句或者無條件轉移語句和下一句語句之間要划分開
  2. 跳轉的目標語句要和上一句語句之間划分開

例如:
\((1)\quad pi:=3.14\\ (2)\quad ar:=0.0\\ (3)\quad n:=16\\ (4)\quad r:=1\\ (5)\quad if\;n<=1\;goto\;(9)\\ (6)\quad r:=r*n\\ (7)\quad n:=n-1\\ (8)\quad goto\;(5)\\ (9)\quad ar:=2*pi\\ (10)\quad ar:=ar*r\\ (11)\quad print\;ar\)

經過划分后:
B1
\((1)\quad pi:=3.14\\ (2)\quad ar:=0.0\\ (3)\quad n:=16\\ (4)\quad r:=1\)
/////////////////////////////////////////////////
B2
\((5)\quad if\;n<=1\;goto\;(9)\)
/////////////////////////////////////////////////
B3
\((6)\quad r:=r*n\\ (7)\quad n:=n-1\\ (8)\quad goto\;(5)\)
/////////////////////////////////////////////////
B4
\((9)\quad ar:=2*pi\\ (10)\quad ar:=ar*r\\ (11)\quad print\;ar\)

流圖

流圖 是在已經划分基本塊的基礎上,構造一個有向圖。

  1. 兩個相鄰基本塊如果上面的沒有跳轉,可以直接和下面的相連
  2. 如果當前基本塊最后存在無條件跳轉,直接和跳轉的目標基本塊相連
  3. 如果當前基本塊存在最后有條件跳轉,需要先和下面相鄰的基本塊相連,然后和跳轉的目標基本塊相連

上面的基本塊集合為\(\{B1,B2,B3,B4\}\),可以用有向邊集合\(\{B1\rightarrow B2, B2\rightarrow B3, B3\rightarrow B2, B2\rightarrow B4\}\),這里不畫圖。

循環

支配結點,指的是對任意兩個結點m和n來說,如果從流圖的首結點出發,到達n的任一通路都要經過m,則稱m是n的支配結點,記為\(m\;DOM\;n\)

下圖是某個程序的流圖,其結點即程序中的基本塊

所有結點的支配結點集D(n):
\(D(1)=\{1\}\\ D(2)=\{1,2\}\\ D(3)=\{1,2,3\}\\ D(4)=\{1,2,4\}\\ D(5)=\{1,2,4,5\}\\ D(6)=\{1,2,4,6\}\\ D(7)=\{1,2,4,7\}\)

該圖的有向邊集合為:\(\{1\rightarrow 2, 2\rightarrow 3, 2\rightarrow 4, 3\rightarrow 4, 4\rightarrow 2, 4\rightarrow 5, 4\rightarrow 6, 5\rightarrow 7, 6\rightarrow 6, 6\rightarrow 7, 7\rightarrow 4\}\)

回邊指的是存在一條邊\(A\rightarrow B\),使得\(B\in D(A)\)。故上圖的回邊有\(4\rightarrow 2, 6\rightarrow 6,7\rightarrow4\)

一個循環由其中的一條回邊\(A\rightarrow B\)對應的兩個結點\(B,A\),以及有通路到達\(A\)而不經過\(B\)的所有結點組成,並且保證\(B\)是該循環的唯一入口結點。

如包含回邊\((6\rightarrow 6)\)的循環為\(\{6\}\)
包含回邊\((7\rightarrow 4)\)的循環為\(\{4,5,6,7\}\)
包含回邊\((4\rightarrow 2)\)的循環為\(\{2,3,4,5,6,7\}\)

這一章可能的考點

  1. 划分基本表、畫出流圖、求支配集、找回邊、找循環
  2. 代碼局部優化

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;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM