目录

如何能深刻了解編譯器?

有用連結:

編譯流程

前端

產生中間語言

請見 Three-Address Code IR

對於中間語言常用的格式,Static single assignment form (SSA),只需要知道底下幾點:

延伸閱讀:

優化中間語言

請見 IR OptimizationGlobal OptimizationGlobal Optimization, Part II

生成目標代碼

Code Generation 包含以下步驟:

可以參考 LLVM 的流程 The high-level design of the code generator

指令選擇

Instruction selection 將 IR 轉換成 machine code,或是低階的 IR (接近目標機器碼),但仍使用 pseudo register,留待之後的 register allocation 將目標機器的暫存器分配給 pseudo register。Instruction selection 一般是將 machine code 模板貼至 IR tree 上,稱為 tiling。一般來說,會將 IR tree 轉成 DAG (directed acyclic graph)。Tree 和 DAG 不同之處在於,節點在 tree 中只有一個父節點,而在 DAG 中可以有多個父節點。DAG 這個特性用來將 common subexpression 暴露出來。若一個節點為 common subexpression,在 tree 中會出現多次,而在 DAG 只會出現一次。DAG 有助於後續的 instruction selection 和 register allocation。

Compilers: Principles, Techniques, and Tools 第 8.9 節: Instruction Selection by Tree Rewriting。

LLVM 使用的是 SelectionDAG based instruction selector。利用 tblgen 生成 instruction selector。

暫存器配置

兩個述句 (statement) 之間的點被稱為 program point。一個變數被 define,代表該變數被寫入值; 一個變數被 use,代表該變數的值被讀出。針對變數 a 而言,

---------- program point
  a = 1; // define
----------
  b = a; // use
----------

一個變數被稱為 live,代表該變數其值在將來會被使用到,亦即在某個時間點被讀出。一個變數的 live range 是一個 program point 的集合,該變數在這些 program ponit 為 live。一個變數的 live interval 是一個最小區間,該區間包含該變數所有的 live range。live interval 相較於 live range 較不精確,但是其 register allocation 較為簡單。Register Allocation 第 64 頁演示的 linear scan 算法就是針對各個變數的 live interval 配置暫存器。編譯器在做 register allocation 時,會視情況針對某變數做 live range splitting,其目的是希望在 live range splitting 之後,能減少各變數之間 live range 重疊的情況。如果兩個變數的 live range 有所重疊,則代表這兩個變數無法分配到同一個暫存器,這會增加暫存器使用量。

Register allocation 包含兩個步驟: 首先,register allocation 釐清哪些變數將可存入暫存器; 接著,register assignment 將物理暫存器分配給那些變數。如果途中發現暫存器不敷使用,register allocation 演算法會選擇將某些暫存器存入內存,這個動作稱之為 spill。register allocation 演算法目標之一就是要降低 spill 的次數。一般來說,register allocation 演算法會選擇把擁有較長 live interval 的變數 spill 到內存。這是因為如果將暫存器分配給該變數,則該暫存器將有較長時間不能再被使用。所以,一般會將暫存器優先分配給 live interval 較短的變數。有時候,如果某暫存器其中的值可以在之後簡單的計算出來,算法可以選擇 spill 該暫存器,且之後不須從內存將其值讀出,只需要重新計算即可,這稱之為 rematerialization

相較於 linear scan 是利用 live interval 此一較不精確的描述來分配暫存器,Register Allocation 第 109 頁演示的 graph coloring 算法利用 live range 分配暫存器。每個變數代表一個節點。如果變數 A 和變數 B 的 live range 有所重疊,則節點 A 和 B 之間有連線。針對各個變數的 live range 所建立的圖稱為 register interference graph (RIG)。此算法目的是要替 RIG 上的節點著色 (代表暫存器),有連線的節點必須著以不同的顏色。

根據 register allocation 的範圍,可分為 local register allocation 和 global register allocation。前者針對一個 basic block 分配暫存器給各個變數,後者則針對數個 basic block。

LLVM 3.0 預計以 greedy register allocation 取代現行的 linear scan 算法。我想你現在具有足夠知識開始閱讀文章 Greedy Register Allocation in LLVM 3.0 了。:-) LLVM 3.0 有兩個 register allocator,分別是 basic allocator 和 greedy allocator。兩者都是根據變數的 live range 分配暫存器。它們都有兩個重要的資料結構: priority queue 和 live interval unions。Priority queue 以變數的 live range 的 spill weight 由高到低排序,將暫存器優先配給 spill 代價較高的變數。Live interval unions 存放的是已經分配到暫存器的 live range,目的是用來檢查某暫存器分配給那些 live range。底下略為描述 basic allocator 和 greedy allocator 的運作方式。

  1. Basic allocator: 由 priority queue 中挑出 spill 代價最高的 live range,在此稱之為 live range A,試圖配置暫存器給它。如果成功,將配置結果更新到 live interval unions; 否則,則 spill live range A。這裡不需要檢查 live interval unions 選擇 spill 其它 live range,因為我們優先配置暫存器給 spill 代價較高的 live range。一般是透過插入 load/store 指令 spill 某個 live range,這會造出新的 live range (在 load 跟 store 之間)。這一個因 spill 造出的 live range 會塞入 priority queue 等待下一次分配,其 spill weight 設為無限大。等到下一次為該 spill weight 為無限大的 live range A 分配暫存器時,basic allocator 會選擇一暫存器,並檢視 live interval unions 看該暫存器分配給那些 live range,亦即與那些 live range 有 interfering。Basic allocator spill 那些倒楣的 live range,再將該暫存器配給該 live range A。
  2. Greedy allocator: 與 basic allocator 的差別在,當沒有適當的暫存器可供分配給 live range A,greedy allocator 會由 live interval unions 中挑出 spill weight 較小的 live range B 並 spill 它 (稱為 evict)。Live range B 會被塞回 priority queue 等待下一次分配。注意! 在 basic allocator 中,不會挑 live interval unions 中 spill weight 較小的 live range 做 spill。此外,geedy allocator 會在 spill live range 之前先試著做 live range splitting,將 live range 分成較小塊的 live range,再將它們塞回 priority queue。

指令排程

請見 Code Optimization

Instruction scheduling 和 register allocation 的順序如何調整及其優缺點請見 The phase order of Instruction Scheduling

Optimization

Link Time Optimization

Exceptio Handling

目前所見的例外處理其實作上基本遵循 Itanium C++ ABI: Exception Handling。它實現所謂的 zero-cost exception handling,也就是例外處理不會造成程式正常執行流程效能上的影響。1)

personality function/routine 是語言執行時期函示庫中的函式,用來作為 system unwind library 和 language-specific exception handling semantics 的介面。這是因為不同的程式語言對於例外處理有不同的行為,personality function 接受例外的內文,包含其型別和值。

LLVM

LLVM 3.0 針對例外處理主要提供以下 LLVM IR/Intrinsics 支援。前端在處理程式語言中的例外處理時,可以將其對映到 LLVM 中用來支援例外處理的指令,再交由之後的優化器和後端處理。底下將會以 C++ 中例外處理的 try/catch/throw 當作對映。

  1. invoke: 針對在 try block 中的函式呼叫,改用 invoke 而非 call。如果在一個會丟出例外的函式中呼叫另一個函式,同樣也是用 invoke。
  2. landingpad: 例外發生之後,會跳轉到一個特殊的基本塊。在這個基本塊中,landingpad 用來準備之後例外處理需要的資料。
  3. resume
  4. llvm.eh.typeid.for

The new LLVM exception handling scheme 說明 LLVM 如何實現例外處理。

try {
   ...
   MayThrowSomething();
   AnotherFunctionCall();
   ...
} catch (int i) {
  DoSomethingWithInt(i);
} catch (class A a) {
  DoSomethingWithA(a);
}

其 try block 對映的 LLVM IR 為,

invoke void @_Z17MayThrowSomethingv() to label %cont unwind label %lpad

我們可以看到在 try block 中的函式呼叫改用 invoke,其分支目標有二: label %cont 和 unwind label %lpad,分別為正常執行流程和例外處理流程。

lpad:
  %info = landingpad { i8*, i32 } personality @__gxx_personality_v0 catch @_ZTIi catch @_ZTl1A
  %except = extractvalue { i8*, i32 } %info, 0
  %selector = extractvalue { i8*, i32 } %info, 1
  %typeid = call i32 @llvm.eh.typeid.for(@_ZTIi)
  %match = icmp eq i32 %selector, %typeid
  br i1 %match, label %run_catch, label %try_next

landingpad 是用來準備例外資訊給後面的例外處理程序使用。landing pad (lpad) 中文意思是停機坪,這代表當例外發生時,執行流程會跳轉到這裡,再轉發到其它地方。%info 是該例外的資訊,{ i8*, i32 } 是其型別。其中 i8* 是 exception pointer,i32 是 selector。personality @__gxx_personality_v0 代表目前處理的是 C\+\+ 的例外,它知道如何將該例外與 catch clause 作 C\+\+ 型別比較。catch @_ZTIi 和 catch @_ZTl1A 則是列出所有的 catch clause,@_ZTIi 和 @_ZTl1A 是語言特定的全域變數,這裡用來代表 catch clause 的 C\+\+ 型別資訊。

術語

硬件平台相關

代碼分析相關

代碼優化相關

附上 LLVM 相關代碼。

代碼保護相關

知名編譯器