Essentials of Compilation筆記
- 標題:Essentials of Compilation: An Incremental Approach in Racket
- 暫譯:編譯的要素——在Racket的遞增臨近法
- 作者:Jeremy G. Siek
- ISBN 9780262047760
- 書源 Repo:https://github.com/IUCompilerCourse/Essentials-of-Compilation/tree/master
概要
- 這基本上是印第安納大學的各編譯器老師的結晶
- 講述必要的觀念、演算法、如何利用映射的觀念將上層軟體對應到硬體
- JIT編譯、程式分析、編譯最佳化(那是閱後的進階課題)
- 編譯器是用許多關卡(pass)轉換而來的。
- nanopass:避免各環節過度耦合
- 另有Python版
- Racket 子集編譯成 x86_64 組語,使用 Linux/Mac OS 的表示法
各章主題
- ch1,2 抽象語法樹、遞迴函數
- ch3 圖着色 graph coloring 問題,以分配記憶體暫存器。
- ch4 條件表達式,將遞迴函數轉換成goto
- ch5 加迴圈與可變量:
- 資訊流分析與暫存器分析
- ch6 加於「堆積」heap分配的tuple(元組)和垃圾回收
- Ch7 加沒有 lexical scoping 的函數(類似 C語言函數)
- 提到呼叫堆疊 stack 和
- 介紹記憶體分配和GC
- Ch8 lexical scoping:閉包如何轉換成 C 語言的 function 和 tuple
- Ch9:動態型別,引入 Any
- Ch10:Gradual Typing
- Ch11:Generics(泛型)
Ch1
提到以下名詞:
- 遞迴函數
- 模式比對
- 抽象語法樹
- 程式語言語法
1.1抽象語法樹 AST
- 表示整數:
(struct Int (value))
- 表示primitive operator 基礎操作子:
(struct Prim (op args))
args是list,可為空,或是任意長度- 也可以表示為:
(struct Read ())
(struct Add (left right))
(struct Neg (value))
- 也可以表示為:
等。
應宜以抽象語法數思考
1.2文法 grammar
- 分成非終端符號(如下方 exp)和終端(terminal)符號如 int。
exp ::= (Prim '+ (exp exp)) | (Prim '- (exp exp)) | (Prim '- (exp))| (Prim read_int ()) | (Int int) L_int ::= (Program '() exp) #對應L_int ::= (Program (info body))
- int 為在此區間的整數:[-2^62, 2^62-1],可以表示2^63個整數
- info 以後會用到,現在不用管
Ch2
- L_var 要漸次變成 x86語言
- L_var 擴展 L_int,就是加上Let綁定
- 加上規則
exp :== Name(var) stmt :==(let(var, exp)) L_var = Module (stmt*)
- 註:要由少漸多的設計
- 要有open recusrion
- 就是要用子型別Lvar繼承Lint,然後必要的時候呼叫super.interp(),其餘時候調用interp()
P.16:我們編譯從P1語言轉成P2語言,再用x86語言編譯就得到想要的結果。 但是也可以用P1的直譯得到同樣的結果。
- global:令主程式空間在外界使用。
- 64bit:計算機記憶體的使用,
- counter:指下一個執行指令的地址
.global main ;x86 main: movq $10,%rax ; $指整數常量,引數還可以用記憶體位置,寄存器名稱。 addq $32, %rax retq
寄存器有16個 movq s, d, $n, callq, retq, jmp label [
- rsp stack堆疊之指標,向下增長
- 減掉其值,則增加stack堆疊之大小。
總之這一段不會組合語言就很難懂。
要定義子語言,建議創立一個instr系列的records,儲存指令,以利模式比對。
使用pass各自解決一個子問題求解
要明確知道選擇,以及我們循序漸進編譯:
- uniqify :唯一化變數名稱
- remove complex operands:primitive處理的運算元變成var or integer
- explicate control:明確化執行順序
- select instruction:轉換L_var到x86
- assign_homes:變數轉換至暫存器或堆疊位置。
- patch instruction
- prelude and conclusion(x86的頭和尾)
Ch3
stack 堆疊速度比較慢 register 暫存器速度比較快
變數無限,寄存器有限,該怎辦?
所以我們要找到那些變數使用(其間牽涉)的起訖時間,然後引用關係做成無向圖,成為圖著色問題(也就是如何讓相鄰的頂點色彩(暫存器)相異,在使用最少的顏色(暫存器)下)。
如果暫存器太少怎辦,就只能spill(灑)到stack堆疊上面。
3-1
組合語言的calling conventions
Lvar
main function (作業系統的call)
內有read_int
我們用SystemV表示法來表示x86_64的組合語言 <- GNU CC 與 Mac OS 使用
caller-saved register(存於caller呼叫者的暫存器)
rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11
其中,call 其他函數的暫存器的引數對應順序:
rdi, rsi, rdx, rcx, r8, r9 (個人註:記憶術disdxc 8 9)
引數太多的話,就用caller frame 的空間
相關說明:
- rax存運算結果
- rcx loop暫存
- rdx 資料暫存
- rsi rdi 索引指位
callee-saved register(存於callee被呼叫者的暫存器)
rbx, rsp, rbp, r12, r13, r14, r15
相關說明
- rbx 基底暫存
- rsp堆疊頂端
- rbp堆疊底端
- call-live variable(會被用的變數)
我們要找到同時使用的變數和不同時使用的變數,來節省暫存器的使用,這是一種圖著色問題。
- 第三章(記憶體分配的「頂點著色問題」跳過)。
- 註:Danny說頂點著色是用最多邊頂點先著色的啟發法,如果寄存器不夠多,就spill放置於記憶體內。
Ch4 加入布林值和if
- (not 1) -> 返回#f
- (car 1) -> 返回error
- 本書用Typed Racket的作法,並且拒絕 (not 1)
Ch 4.1 布林運算
加入布林型別,還有二元運算,還有p.63的type checker 相關語法如下:
(cmp atom atom) (not atm) (goto label)
if (cmp atm atm) goto label else goto label
(if::= (label tail) ...
x86程式也添加bytereg
instr : xorq, cmpq, set, movzbq, jmpif, ...
(and e1 e2) = (if e1 e2 #f)
(or e1 e2) = (if e1 #t e2)
type check class
operate_types = function( ){ {"+", ((int int) -> int)}, {"-", ((int int) -> int)}, ... }
List 要逐項確認型別 map(型別 list)
Ch5 迴圈和資料流分析
#<void>
也是value,是副作用的set!傳回結果set!
方能動態更改值 * 用資料流分析,來分析各變數的生存長度!- “Kleene fixed-point analyzation”
(begin! (set! x 40) x)
變成(begin (set! x 40) (get! x))
uncover-get!
ch6 tuple(類似陣列)和垃圾回收
- tuple(類似array),用compluter的heap -> tuple 的生命週期為定義
- 可是要清掉,不然會out of memory,所以需要垃圾回收
- 相關程式碼:
(vector x1 x2 x3) vector-ref vector-set! (vector-length exp) type ::= (vector type*)
- vector 沒有動態的scope
- garbage collection ->
- 保留活變數
- 釋放垃圾
- p.102 垃圾回收
- 本書未另外教學如何寫垃圾回收
- 本書使用Cheney’s algorithm來辦理垃圾回收
- Heap有tuple等object
- 分兩區,runtime時互換:
- FromSpace
- ToSpace
- 拷貝自heap有連結的stack,至另一個tospace
- 不連結引用的,不拷貝。
- p.104 cheney’s algorithm看不太懂
- Ch 6.2.3 資料代表:本書選用1,2兼用:
- 物件加上tag表示指標與否
- 存在不同資料分區
- 用型別資訊來處理
- 指標遮罩 (pointer mask)
- 垃圾回收器在runtime.c實作
- 6.3 展示分配 (expose allocation)
exp :== (collect int) | (allocate int type) | (global-value name)
Ch 7 函數
- p.127 講函數實作:
(apply exp exp*)
type ::= (type ... -> type) exp ::= (exp exp...) def ::= (define (var [var : type] [var : type] ...) : type exp) L_fun ::= def .... exp (def (inc [x : integer]): integer (+ x 1))
- 呼叫函數:
leaq inc(%rip) %rbx callq *%rbx
rax
放置return value- 參數置放區:
rdi rsi rdx rcx r8 r9
- 參數小於6個,超過6個,多出來的放進去向量:
f(x1, x2, x3, x4, x5, [x6, x7, ....])
- 參數小於6個,超過6個,多出來的放進去向量:
- 尾遞迴最佳化
ch8 lexical function scoping
(define (f [x : int]) : (int -> int) let y = 4 return lambda (z : int) : int => {x + y + z})
上面的式子裡,lambda 是 lexical scoping 相關的匿名函數,其實是閉包。
本章講到flat closure的實作,以及conversion
exp ::= (lambda [(var : type)...]) : type exp) (procedyre-arity exp) ; 求exp (function) 的參數數量
L_lambda = def ... exp
fv variable要放在heap上面,但需要boxing變數
- p.151 實作兩個函數
free_variable
fv_in_lambda
- let 的被代入變數以及是
lambda
的自由變數,方需要boxing convert_assignment
來提到這個問題:(box <-> unbox)- 把 free_variable 轉成vector (heap 上)
(Var x)
- p.153
(lambda parameters return_type body) -> (closure 參數數量 (cons (FunRef name 參數) free_variables))
- closure conversion 看不太懂
def f6(x7): y8 = 4 return lambda(z9){x7 + y8 + z9} let x = 3 let f = lambda (y){x + y + 3}; lambda (ps){body} // ps 指參數 => lambda (ps, fv) => {body} // fv 指自由變數 let fv = [x]// 這裡的fv 可以用 fv = malloc(x); fv[0] = x; ... 這樣的 f3(x, b, c)=> f3(fv[3], b, c)
Ch9 dynamic typing
動態型別 + polymorphism
exp ::= (exp ...) | | (lambda (var ...) exp) | (boolean? exp) | (integer? exp) | (vector? exp) | (procedure? exp) | (void? exp) def ::= (define (var var...) exp) L_dyn ::= def ... exp tagged_value = structTagged {value, tag(type)} #: transparent tagof(integer) = (001)_2 tagof(boolean) = (002)_2 ... 用數字表示型別tag(型別編號) (Inject exp type) # 轉為tagged (Project exp type) # 取消轉為tegged any-vector-length | any-vector-ref | any-vector-set!
Ch10 gradual typing
- p.179 L_? : 混合靜態、動態型別 -> type_annotation,有無gradual typing
parameter ::= var | [var : type] ret ::= ε|type * (cast val type1 type2) ; 改變型別 * 看不太懂gradual type
- 經過ChatGPT的說明,這是混合不標型別和強制標型別的方式。但是對我來說沒什麼用,不如全部強制指定型別,一來提昇執行速度,二來讓使用者不會誤解一個物件的型別
Ch 11 多型實作方法
map
可為 (forall (T) (T->T) -> (vector T) -> (vector T))
這需要型別推論設計
要轉成函數,可以用
- uniform intersection: tagged box化的物件,設計函數於執行時偵測型別,再依標籤決定處理型別,但效率慢
- 轉成多個處理單型別的函數,但是code生成會很多,而且沒有擴展性
附錄:指令集摘
指令名 | 操作說明 | 註 |
---|---|---|
addq A B
|
A + B -> B | |
negq A
|
-A | |
subq A B
|
B - A -> B | |
imulq A B
|
A * B -> B | |
popq A
|
*rsp -> A; rsp + 8 -> rsp | 跳出(pop),rsp再往大移 |
push A
|
rsp - 8 -> rsp; A -> *rsp | rsp往小移,再壓入(push) |