內容提要
在我們寫的程式碼中,有若干個變數,若干個函式;變數還會重名,還有值。編譯器卻總能找到我們指定的變數或函式,從不找錯人。在我看來,這是一個很神奇的功能。剖析一番,會發現”符號表“的身影。
符號表,儲存變數的值、函式。變數作用域依賴它,找到正確的變數也依賴它。
一起來看看符號表吧。
符號
老規矩,先從一段程式碼開始。
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 編譯器剖析》