編譯器的符號表管理


內容提要

在我們寫的代碼中,有若干個變量,若干個函數;變量還會重名,還有值。編譯器卻總能找到我們指定的變量或函數,從不找錯人。在我看來,這是一個很神奇的功能。剖析一番,會發現”符號表“的身影。

符號表,存儲變量的值、函數。變量作用域依賴它,找到正確的變量也依賴它。

一起來看看符號表吧。

符號

老規矩,先從一段代碼開始。

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;
}

從上面的代碼中抽取下列元素:

  1. DataColor
  2. RED, GREEN, BLUE
  3. a,b,c,d
  4. INT32
  5. "hello,world"
  6. Begin

這些元素,每個都用一種數據結構存儲。這種數據結構,我們把它叫做“符號”,對應的英文術語是“Symbol”。

細心一點的讀者朋友會發現:並不是每行只有一個元素,有的行有多個元素。寫在同一行的元素屬於同一個類別。

在C語言中,有哪幾種類別的語言元素呢?先給出下面幾種類別。這些類別和上面的那幾行元素一一對應。請看。

  1. SK_Tag
  2. SK_EnumConstant
  3. SK_Variable
  4. SK_TypedefName
  5. SK_String
  6. SK_Lable

除了上述六種類別,還有這些類別。

  1. SK_Register。例如 eax
  2. SK_IRegister。例如 (eax)
  3. SK_Tmp。臨時變量。
  4. SK_Offset。后面再將這個類別。
  5. 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。后面解釋。

現在可以說說什么是符號了。符號,用來存儲變量或常量的數據結構。

仿照上面的例子,比較容易知道怎么存儲ColorINT32等語言元素,我就不一一示范了。

公共表達式

什么是公共表達式

在上面,我介紹了符號的數據結構,但那並不是符號的最終版本。

是的,我要補充新內容了,公共表達式。

還是先看代碼吧。

// 從前面的例子中抽取出來的。
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

cd的值都是a+b。生成一個臨時變量t0,存儲a+b的值。把t0賦值給cd。在這個過程中,只計算了一次a+ba+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中新增了兩個成員defuses

每個變量參與了哪些表達式,用uses記錄。uses是一個ValueDef的單鏈表。

存儲公共表達式實例

完善一下存儲a的符號。如下。

成員 成員值
kind SK_Variable
ty int
name a
aname
offset 0
uses def0

t0:a+bValueDef記錄,記作def0

dst t0
op +
src1 存儲a的VariableSymbol
src2 存儲b的VariableSymbol
link

t1:a+bValueDef記錄,記作def1

dst T1
op +
src1 存儲a的VariableSymbol
src2 存儲b的VariableSymbol

怎么記錄t0呢?用VariableSymbol。請看下面。

成員 成員值
kind SK_Tmp
ty int
name t0
aname
offset 0
def def0

比較一下at0的符號,至少有3個差異:

  1. t0kindSK_Tmp,因為 t0是一個臨時變量。
  2. name不同。
  3. def不同。我認為,只有臨時變量的符號的def才有意義。

臨時變量的符號的uses有意義嗎?

換個問法:需要記錄臨時變量參與過的表達式嗎?我也不知道。

哈希表

開放散列法

如果a+b已經出現過一次,再次遇到a+b時,將不再計算a+b。那么,怎么才能知道a+b有沒有出現過呢?方法是,把a+b存儲起來。

存儲一個表達式,用哈希表很合適。我們使用的這個哈希表使用“開放散列法”創建。具體方法如下:

  1. 哈希key = (src1 + src2 + op) / 16;
  2. key相同、但實際上不同的表達式組成一個單鏈表。
    1. 這個單鏈表中的結點的數據類型是 ValueDef
    2. 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中。

AB一起構成哈希鏈表。

哈希鏈表的數據結構

bucketLinker

bucketLinker的數據結構如下。

// 哈希表的大小。
#define BUCKET_SIZE 128
// 計算哈希key的掩碼。
#define BUCKET_HASH_MASK 127

typedef struct bucketLinker{
  Symbol sym;
   // 指向下一個bucketLinker。
   struct bucketLinker *link;
}*BucketLinker;

Symbol有兩個成員linknext,都能指向下一個Symbol。為什么還新建一個BucketLinker用來創建鏈表呢?因為Symbol的兩個成員linknext都有其他用途,暫且不關注。

哈希表

再看哈希表的數據結構。

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語言中,存在變量、函數。要為一個變量建模,就要設計出合適的結構存儲變量的數據類型和變量名、變量值。

用類型系統存儲數據類型。

用符號表存儲變量的變量名、變量值、變量的初始值等。

符號表需具備下列功能:

  1. 存儲變量的變量名、變量值、變量的初始值等;存儲函數的函數名、參數值、函數體等。
    1. 上面這句話不全對。對錯依賴編譯器設計者的具體設計。
    2. 讀者朋友理解為:存儲變量值或函數體等。
  2. 作用域。
  3. 查找變量、函數等。

對了,想了解類型系統,請閱讀《C語言的類型系統》。

參考資料

《C 編譯器剖析》


免責聲明!

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



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