內容提要
在我們寫的代碼中,有若干個變量,若干個函數;變量還會重名,還有值。編譯器卻總能找到我們指定的變量或函數,從不找錯人。在我看來,這是一個很神奇的功能。剖析一番,會發現”符號表“的身影。
符號表,存儲變量的值、函數。變量作用域依賴它,找到正確的變量也依賴它。
一起來看看符號表吧。
符號
老規矩,先從一段代碼開始。
struct Data{
int num1;
int num2;
}dt;
Enum Color{RED, GREEN, BLUE};
int a,b,c,d;
typedef int INT32;
int main(int argc, char *argv[]){
char *hi = "hello,world";
Begin:
c = a + b;
d = a + b;
a = 3;
dt.m2 = a + b;
return 0;
}
從上面的代碼中抽取下列元素:
-
Data
、Color
-
RED, GREEN, BLUE
-
a,b,c,d
-
INT32
-
"hello,world"
-
Begin
這些元素,每個都用一種數據結構存儲。這種數據結構,我們把它叫做“符號”,對應的英文術語是“Symbol”。
細心一點的讀者朋友會發現:並不是每行只有一個元素,有的行有多個元素。寫在同一行的元素屬於同一個類別。
在C語言中,有哪幾種類別的語言元素呢?先給出下面幾種類別。這些類別和上面的那幾行元素一一對應。請看。
-
SK_Tag
-
SK_EnumConstant
-
SK_Variable
-
SK_TypedefName
-
SK_String
-
SK_Lable
。
除了上述六種類別,還有這些類別。
-
SK_Register
。例如eax
。 -
SK_IRegister
。例如(eax)
。 -
SK_Tmp
。臨時變量。 -
SK_Offset。后面再將這個類別。 -
SK_Function
。存儲函數。
符號的數據結構
Enum SymbolKind {SK_Tag, SK_EnumConstant, SK_Variable, SK_TypedefName, SK_String, SK_Lable, SK_Register, SK_IRegister, SK_Tmp, SK_Offset};
#define SYMBOL_COMMON \
// kind的取值是SymbolKind中的枚舉值。
int kind; \
// 變量的數據類型。
Type ty; \
// 變量名,例如int a中的a。
char *name; \
// 這個變量的別名,以后會用到。
char *aname; \
// 當kind是SK_Offset時,這個值才有意義。
int offset;
typedef struct symbol{
SYMBOL_COMMON
}*Symbol;
一般不直接使用Symbol
存儲變量,而是使用它的“子結構”,VariableSymbol
。
typedef struct VariableSymbol{
SYMBOL_COMMON
}*VariableSymbol;
馬上來用VariableSymbol
存儲int a
。
成員 | 成員值 |
---|---|
kind | SK_Variable |
ty | int |
name | a |
aname | |
offset | 0 |
存儲dt
。
成員 | 成員值 |
---|---|
kind | SK_Tag |
ty | struct Dt。派生類型。 |
name | dt |
aname | |
offset | 0 |
存儲dt
的成員num2
。
成員 | 成員值 |
---|---|
kind | SK_Offset |
ty | int |
name | num |
aname | |
offset | 4。后面解釋。 |
現在可以說說什么是符號了。符號,用來存儲變量或常量的數據結構。
仿照上面的例子,比較容易知道怎么存儲Color
、INT32
等語言元素,我就不一一示范了。
公共表達式
什么是公共表達式
在上面,我介紹了符號的數據結構,但那並不是符號的最終版本。
是的,我要補充新內容了,公共表達式。
還是先看代碼吧。
// 從前面的例子中抽取出來的。
c = a + b;
d = a + b;
a = 3;
dt.m2 = a + b;
這段代碼對應的中間代碼如下。
t0: a+b
c: t0
d: t0
a: 3
t1: a+b
dt[4]: t1
c
和d
的值都是a+b
。生成一個臨時變量t0
,存儲a+b
的值。把t0
賦值給c
、d
。在這個過程中,只計算了一次a+b
。a+b
就是“公共表達式”。
引入“公共表達式”,能減少最終生成的機器指令,提高程序的執行效率。
怎么存儲公共表達式
我們設計的VariableSymbol
能存儲公共表達式嗎?
我在想什么?
在想,用什么理由引出valueUse最合適。
想不到!
dt[4]
的值為什么不是t0
而是t1
?因為a
被修改了,t0
不能再作為公共表達式。
一旦公共表達式中的變量被修改,這個變量參與過的所有公共表達式都將失效,不再被當作公共表達式使用。我們設計的符號管理機制必須實現這個功能。
typedef struct valueDef{
Symbol dst;
int op;
Symbol src1;
Symbol src2;
struct valueDef *link;
}ValueDef;
typedef struct valueUse{
ValueDef def;
struct valueUse *use;
}ValueUse;
#define SYMBOL_COMMON \
// kind的取值是SymbolKind中的枚舉值。
int kind; \
// 變量的數據類型。
Type ty; \
// 變量名,例如int a中的a。
char *name; \
// 這個變量的別名,以后會用到。
char *aname; \
// 當kind是SK_Offset時,這個值才有意義。
int offset;
//
ValueDef def;
//
ValueUse uses;
我在SYMBOL_COMMON
中新增了兩個成員def
和uses
。
每個變量參與了哪些表達式,用uses
記錄。uses
是一個ValueDef
的單鏈表。
存儲公共表達式實例
完善一下存儲a
的符號。如下。
成員 | 成員值 |
---|---|
kind | SK_Variable |
ty | int |
name | a |
aname | |
offset | 0 |
uses | def0 |
t0:a+b
用ValueDef
記錄,記作def0
。
dst | t0 |
---|---|
op | + |
src1 | 存儲a的VariableSymbol |
src2 | 存儲b的VariableSymbol |
link |
t1:a+b
用ValueDef
記錄,記作def1
。
dst | T1 |
---|---|
op | + |
src1 | 存儲a的VariableSymbol |
src2 | 存儲b的VariableSymbol |
怎么記錄t0呢?用VariableSymbol
。請看下面。
成員 | 成員值 |
---|---|
kind | SK_Tmp |
ty | int |
name | t0 |
aname | |
offset | 0 |
def | def0 |
比較一下a
和t0
的符號,至少有3個差異:
-
t0
的kind
是SK_Tmp
,因為t0
是一個臨時變量。 -
name
不同。 -
def不同。我認為,只有臨時變量的符號的def才有意義。
臨時變量的符號的uses有意義嗎?
換個問法:需要記錄臨時變量參與過的表達式嗎?我也不知道。
哈希表
開放散列法
如果a+b
已經出現過一次,再次遇到a+b
時,將不再計算a+b
。那么,怎么才能知道a+b
有沒有出現過呢?方法是,把a+b
存儲起來。
存儲一個表達式,用哈希表很合適。我們使用的這個哈希表使用“開放散列法”創建。具體方法如下:
-
哈希key = (src1 + src2 + op) / 16; -
key相同、但實際上不同的表達式組成一個單鏈表。 -
這個單鏈表中的結點的數據類型是 ValueDef
。 -
ValueDef
的成員link
用來創建單鏈表。
-
這個哈希表在哪里?我把它放在函數的符號中。來看一看存儲函數的結構。
存儲函數的符號
typedef struct functionSymbol{
SYMBOL_COMMON
// 存儲函數的參數。
Symbol params;
// 存儲函數的局部變量。
Symbol locals;
// 存儲公共表達式的哈希表。
ValueDef valNumTable[16];
}FunctionSymbol;
公共表達式經過哈希函數處理之后,存儲到FunctionSymbol的成員valNumTable中。
這意味着,我們只處理函數中的公共表達式。對函數外面的公共表達式,不處理。
使用哈希表的實例
一起看一看怎么把t0:a+b
存儲到哈希表中。偽代碼如下。
int key = ((int)src1 + (int)src2 + op) / 16;
ValueDef head = valNumTable[key];
ValueDef current = head;
ValueDef target = NULL;
while(current != NULL){
if(current->src1 == src1 && current->src2 == src2 && current->op == op){
target == current;
break;
}
current = current->link;
}
if(target == NULL){
ValueDef tmp;
tmp->op = op;
tmp->src1 = src1;
tmp->src2 = src2;
tmp->dst = t0;
// 存儲到哈希表中。
tmp->link = valNumTable[key];
valNumTable[key] = tmp;
}
上面的偽代碼正確展示了用”開放散列法“創建哈希表,忽略了很多關於”公共表達式“的邏輯。
在哈希表中找到公共表達式后,如果這個表達式中的src1或src2已經被修改過,這個公共表達式就是無效的。
另一個哈希表
用上一個哈希表,我們解決了存儲a+b
的問題。現在,我們面臨新的問題:
a
存儲在Symbol
,大量的Symbol
存儲在哪里?
依然存儲在哈希表中。這些哈希表還會構成一個單鏈表。
為什么要創建哈希表鏈表
在C語言中,存在”作用域“這個概念,英文術語是scope
。
int a;
int test(){
int a = 5;
return 0;
}
上面的代碼中有兩個a
,但這兩個a
的作用域不同。
全局變量a
存儲在哈希表A
中,test
的局部變量a
存儲在哈希表B
中。
A
和B
一起構成哈希鏈表。
哈希鏈表的數據結構
bucketLinker
bucketLinker
的數據結構如下。
// 哈希表的大小。
#define BUCKET_SIZE 128
// 計算哈希key的掩碼。
#define BUCKET_HASH_MASK 127
typedef struct bucketLinker{
Symbol sym;
// 指向下一個bucketLinker。
struct bucketLinker *link;
}*BucketLinker;
Symbol
有兩個成員link
和next
,都能指向下一個Symbol
。為什么還新建一個BucketLinker
用來創建鏈表呢?因為Symbol
的兩個成員link
和next
都有其他用途,暫且不關注。
哈希表
再看哈希表的數據結構。
typedef struct table{
// BucketLinker就存儲在table中,實質是變量Symbol或函數Symbol存儲在table中。
Symbol buckets[BUCKET_SIZE];
// 哈希表的層次。
int level;
struct table *outer;
}*Table;
level
存儲”作用域“。在前面的例子中,存儲全局變量a
的哈希表的level
的值是0,存儲test
的局部變量a
的哈希表的level
的值是1。
AddSymbol
本節的小標題是一個函數名,這個函數的功能是:把一個存儲了變量或函數的Symbol
存儲到哈希表中。請看偽代碼。
AddSymbol(Symbol sym, Symbol *table){
int key = (int)sym / BUCKET_HASH_MASK;
// 創建一個BucketLinker
BucketLinker linker;
linker->sym = sym;
// 如果table中沒有存儲BucketLinker鏈表,把新linker作為鏈表的第一個結點
// 存儲到table中。
if(table[key] == NULL){
table[key] = linker;
}else{
// 如果table中存儲了BucketLinker鏈表,把新linker添加到鏈表的頭結點,
// 然后把新鏈表也就是鏈表的頭結點存儲到table中。
// 把sym存儲到哈希表中。
linker->link = table[key];
table[key] = linker;
}
}
再看一小段代碼。
linker->link = table[key];
table[key] = linker;
這不就是從AddSymbol
中抽出來的嗎?有必要再看一次。
因為這兩行代碼運用了”開放散列法“,而我很久以前覺得”開放散列法“是比較有技術含量東西。
在我的工作中,未曾需要自己動手實現”開放散列法“。用到哈希時,調用一個哈希函數而已。
總結
關於符號表,就講完了。本文囊括了主要知識點。
第一次看符號表,我覺得有點復雜。慚愧慚愧。
做個簡單的總結吧。
在編程語言例如C語言中,存在變量、函數。要為一個變量建模,就要設計出合適的結構存儲變量的數據類型和變量名、變量值。
用類型系統存儲數據類型。
用符號表存儲變量的變量名、變量值、變量的初始值等。
符號表需具備下列功能:
-
存儲變量的變量名、變量值、變量的初始值等;存儲函數的函數名、參數值、函數體等。 -
上面這句話不全對。對錯依賴編譯器設計者的具體設計。 -
讀者朋友理解為:存儲變量值或函數體等。
-
-
作用域。 -
查找變量、函數等。
對了,想了解類型系統,請閱讀《C語言的類型系統》。
參考資料
《C 編譯器剖析》