本文最初是基於對新員工培訓, 使其快速上手編譯器后端代碼而寫的入門簡介. 為方便閱讀又根據模塊細分為若干章, 內容以分析代碼為主, 偶爾也會穿插一些理論擴展.
什么是指令選擇
指令選擇(instruction selection)是將中間語言轉換成匯編或機器代碼的過程. 如果僅為單一語言在單一目標上實現指令選擇, 可以使用手工編碼的方法. 否則通過使用自動代碼生成器生成代碼, 編譯器開發人員只需負責修改不同目標的機器指令描述, 是更優選擇.
指令選擇需要考慮的問題
- 目標的寄存器, 尋址方式和指令體系結構
目標的指令集與體系結構會影響指令選擇的方式. e.g. 在X86上給一個寄存器賦值32bit立即數地址只需要mov一條指令, 但像ARM與RISCV等架構只支持12bit的立即數加法, 需要更加復雜的方式實現同樣的移動語義. 根據codesize model的設置, RISCV可以使用lui + addi兩條指令實現(分別移動高20bit與低12bit), 或將地址保存到當前指令地址附近然后用addi + lw兩條指令實現(計算常量所在地址並訪問該地址). - 軟件調用約定(calling convention)
指令選擇必須考慮兼顧目標的ABI定義. - 中間語言的結構和特征
本質上中間語言對生成的代碼的正確性沒有影響, 然而中間語言的結構影響對自動代碼生成算法的設計. - 轉化為機器代碼的方式
略.
常見的指令選擇實現
常見的指令選擇實現可以參見經典書籍Survey on Instruction Selection(這塊內容以后有空單獨再介紹).
- 宏展開(macro expension)
- 樹覆蓋(tree covering)
- 有向圖覆蓋(DAG covering)
LLVM當前的指令選擇實現
LLVM在O0編譯時使用名為FastISel的指令選擇PASS, 在O2編譯時使用名為SelectionDAG的指令選擇PASS, 同時當前社區還在推進名為GlobalISel的指令選擇PASS(當前僅在AArch64上支持), 希望能替代SelectionDAG. 我們將首先介紹SelectionDAG(如未特殊說明, 下文介紹均默認為SelectionDAG), 然后會簡要介紹FastISel與GlobalISel, 並比較三者特點, 討論為什么要使用GlobalISel替換SelectionDAG.
什么是SelectionDAG
SelectionDAG是一種trees-on-DAGs的有向圖覆蓋實現, 編譯器開發人員通過編寫指令的樹匹配(tree pattern), 通過tablgen翻譯成完整的樹匹配代碼交由代碼生成器(matcher generator)處理, 后者采用貪婪的DAG-to-DAG策略將中端IR覆寫為機器指令描述.
SelectionDAG流程簡介
SelectionDAG由若干個優化組合而成, 其具體流程圖如下所示, 其中紅色節點表示數據(輸入的中間語言)的組成形式, 黑色節點表示處理模塊.
我們會依據SelectionDAG的流程依次分析每個步驟的目標, 實現方式以及在支持一個新架構時需要修改的注意點, 這里先簡要介紹各個模塊的作用:
- 上文提到SelectionDAG是基於DAG covering的指令選擇實現, 因此需要首先將輸入程序流的表示方式從IR轉換為DAG, 該過程又被稱做lowering. lowering會將IR節點一一的翻譯為DAG節點的同時也會處理調用約定, 使其遵守特定目標的ABI規范(這也是lowering名字的含義). 在lowering后程序流是以名為SDNode的節點組成的DAG.
- 根據特定目標所支持的指令集與寄存器結構, SelectionDAG將目標不支持的操作與數據類型轉換為合法的操作與數據類型, 這一步被稱作legalize. 另一方面對於其中產生的可以合並的操作被稱作combine. 可以看到legalize與combine執行了多次, 且legalize每次執行的內容都不一樣.
- 在combine和legalize之后是select步驟, 通過pattern match或手寫代碼的方式生成對應指令. 經過指令選擇后的程序流是以Machine Instruction為節點組成的DAG.
- 在早期的SelectionDAG實現里還考慮的了調度的優化, 然而現在LLVM已經支持了PreRA與PostRA的調度, 在此處調度優化並不重要, 本文暫不涉及, 關於調度的實現將在以后調度器實現分析中介紹.
- 指令選擇(步驟3)只是選出了對應的指令, 還需要將其重新覆寫為基於SSA格式的Machine Instruction描述, 為每條指令分配虛擬寄存器, 連接PHI節點.