1. 程式人生 > >llvm學習筆記(1)

llvm學習筆記(1)

1.      概述

指令選擇(instruction selection,也稱為程式碼選擇,有時甚至稱為程式碼生成)是編譯器程式碼生成器後端所涉及的重要問題之一。另外兩個重要問題是指令排程與暫存器分配。指令選擇器負責通過儘可能地用好可用的機器指令,將程式從目標機器無關的表示翻譯到一個目標機器特定的形式。這使得兩個正交的子問題必須得到解決:

1.      檢測何時及何地使用某條機器指令是可能的,並且

2.      在存在多個選項時,決定選擇哪條指令。【1】

實際上Tablegen對後兩個問題也提供了相當程度自動化的支援。不止於此,Tablegen還對LLVM的整體式(integrated)彙編器提供了強大的自動化支援。彙編碼的解析,目標機器格式的編解碼,彙編檔案與目標檔案的讀寫等功能的相當部分程式碼,都可由Tablegen根據TD描述檔案自動生成。另外,Tablegen所基於的TD描述格式有高度的可擴充套件性,比如這篇部落格所描述的

對Hexagon目標機器所進行的指令關係框架的擴充套件

LLVM的指令選擇是基於DAG(directedacyclic graph,有向無環圖)上的樹模式匹配【1,2】。其指令選擇器生成的自動化程度很高。LLVM內部使用一個具有C++相似語法的高階語言對機器進行描述,而Tablegen解析這些描述,並生成指令選擇器的大部分原始碼(LLVM仍然需要定製部分原始碼)。這些描述檔案的字尾是“.td”(為了方便我們稱之為TD語言),它們分佈在llvm/lib/Target目錄下面的各個特定體系架構目錄。以X86為例,包括下列檔案(來源:LLVM-3.6):

X86.td:對X86架構的描述。

X86CallingConv.td:X86架構的呼叫規範。

X86InstrInfo.td:X86架構的基本指令集。

X86InstrMMX.td:MMX指令集。

X86InstrMPX.td:MPX(MemoryProtection Extensions)指令集。

X86InstrSGX.td:SGX(Software GardExtensions)指令集。

X86InstrSSE.td:SSE指令集。

X86InstrSVM.td:AMD SVM(Secure VirutalMachine)指令集。

X86InstrTSX.td:TSX(TransactionalSynchronziation Extensions)指令集。

X86InstrVMX.td:VMX(Virtual MachineExtensions)指令集。

X86InstrSystem.td:特權指令集。

X86InstrXOP.td:對擴充套件操作的描述。

X86InstrFMA.td:對融合乘加指令的描述。

X86InstrFormat.td:對X86指令格式定義的描述。

X86InstrFPStack.td:對X86浮點單元指令集的描述。

X86InstrExtension.td:對零及符號擴充套件的描述。

X86InstrFragmentsSIMD.td:描述SIMD所使用的模式片段。

X86InstrShiftRotate.td:對shift及rotate指令的描述。

X86Instr3DNow.td:對3DNow!指令集的描述。

X86InstrArithmetic.td:對X86架構的算術指令的描述。

X86InstrAVX512.td:對X86 AVX512指令集的描述。

X86InstrCMovSetCC.td:對X86條件move及設定條件指令的描述。

X86InstrCompiler.td:由編譯器使用的各種偽指令,及指令選擇過程中使用的Pat模式。

X86InstrControl.td:描述X86 jump,return,call指令。

X86RegisterInfo.td:對X86體系暫存器的描述。

X86SchedHaswell.td:對Haswell機器模型的描述。

X86SchedSandyBridge.td:對Sandy Bridge機器模型的描述。

X86Schedule.td:X86體系指令排程的一般描述。

X86ScheduleAtom.td:用於Intel Atom處理器指令排程。

X86SchedSandyBridge.td:用於Sandy Bridge機器模型的指令排程。

X86SchedHaswell.td:用於Haswell機器模型的指令排程。

X86ScheduleSLM.td:用於Intel Silvermont機器模型的指令排程。

X86ScheduleBtVer2.td:用於AMD btver2 (Jaguar) 機器模型的指令排程。

如此複雜的描述顯然與X86是一個CISC機器,而且具有多種架構有關。由此也可以看出Tablegen實在是一個強大的工具。

另外,在目錄llvm/include/llvm/target下包含了與體系架構無關的、公用的描述定義:

Target.td:每個目標機器都要實現的體系架構無關的介面。

TargetItinerary.td:使用instruction itineraries進行排程的機器所實現的體系架構無關的排程介面。

TargetSchedule.td:使用基於Tablegen排程的機器所實現的體系架構無關的排程介面。

TargetSelectionDAG.td:SelectionDAG指令選擇排程器使用的體系架構無關的排程介面。

TargetCallingConv.td:用於描述呼叫慣例的體系架構無關的介面。

在目錄llvm/include/llvm/CodeGen下包含檔案ValueTypes.td用於描述暫存器與運算元的型別。這是在整個LLVM範圍內公用的。

在目錄llvm/include/llvm/IR下包含檔案Intrinsics.td用於描述通用的固有函式,以及與體系架構相關的固有函式,以X86為例,這個檔案是IntrinsicsX86.td。另外,檔案IntrinsicsNVVM.td描述了NVIDIA NVPTX的固有函式。檔案IntrinsicsAArch64.td則是描述了64位系統的固有函式。

編譯的本質是將高階語言寫成的程式翻譯為一個目標機器的指令序列,保證不改變語義,同時儘可能使該指令序列能在最小的執行時間內執行完成。為了儘快地執行指令,CPU使出了渾身解數,比如流水線,比如VLIW,比如向量機,等等。因此在編譯器眼裡,指令不只是一串二進位制碼那麼簡單。為了充分利用CPU提供的效能加速,編譯器必須瞭解每一條指令執行所需的時間、所需的CPU資源(暫存器、計算單元、流水線佔用等)、暫存器與記憶體使用的約束條件等等。另一方面,編譯器也需要知道目標機器的種種細節,包括有哪些暫存器,這些暫存器的用途,有哪些功能單元,功能單元的效能、時序,諸如此類。

編譯器利用這些資訊進行:

·        指令選擇——選擇既能完成指定操作,執行時間又最短的指令。

·        指令排程——根據指令間的依賴關係(不像在高階語言程式中出現的幾種語句間依賴關係,這裡依賴關係有:輸出依賴——一條指令的輸出作為另一條指令的輸入;功能單元的競爭:功能單元的執行有一定的時間週期,而且只能同時容納一條指令),進行指令重排,以期更高效地利用CPU的功能單元。

·        暫存器分配——在指令選擇過程中,暫存器被認為是無限的,但實際機器總是隻有有限的暫存器,而且這些暫存器也一定各有用途。因此,編譯器必須保證目標機器的暫存器集能滿足產生的指令序列。顯然,指令選擇期間必須將使用了多少暫存器、使用哪些暫存器作為選擇指令的一個指標。

1.1.            DAG指令選擇生成器

在LLVM的線上文件“The LLVM Target-Independent Code Generator”這樣描述指令選擇生成器:

TableGenDAG指令選擇器生成器讀入.td檔案中的指令模式,並自動為你的目標機器構建模式匹配程式碼的各個部分。它有以下優點:

·        在編譯編譯器時刻,它分析你的指令模式並告訴你你的模式是否合理。

·        它可以處理用於模式匹配的運算元上任意的約束。特別的,描述類似“匹配任意13位符號擴充套件值的立即數”,它是直觀的。例如,參考PowerPC後端的immSExt16及相關的tblgen類。

·        它知道模式定義的幾個重要的特性。例如,它知道加法是符合交換律的,因此它允許上面的FMADDS模式匹配“(fadd X, (fmul Y, Z))”以及“(fadd (fmul X, Y), Z)”,不需要目標機器作者特殊地處理這個案例。

·        它具有完善的型別推導系統。特別的,你很少需要明確告訴系統你模式各部分是什麼型別。在上面的FMADDS例子中,我們不需要告訴tblgen模式中所有的節點具有型別‘f32’。從F4RC(作者注:這是一個暫存器類別)具有型別‘f32’這個事實,它能推導並傳播這個認識。

·        目標機器可以定義自己的(並且依賴內建的)“模式片段”。模式片段是可重用的模式塊,在編譯編譯器時刻內聯進你的模式。例如,整形的“(not x)”操作實際上被定義為一個展開為“(xor x, -1)”的模式片段,因為SelectionDAG沒有一個原生的‘not’操作。目標機器可以視情況定義自己的速記片段。例子參考‘not’及‘ineg’的定義。

·        除了指令,目標機器可以使用‘Pat’類詳細說明任意對映到一條或多條指令的模式。例如,PowerPC沒有辦法在一條指令中載入一個任意整形立即數。要告訴tblgen如何做到,這樣定義:(作者注:在這裡Pattern與Pat沒有本質區別,Pat只是在第二個模板引數外比Pattern少寫一對“[]”)

// Arbitrary immediate support.  Implement in terms of LIS/ORI.
def : Pat<(i32 imm:$imm),
          (ORI (LIS (HI16 imm:$imm)), (LO16 imm:$imm))>;

如果向暫存器載入一個立即數的單條指令模式都匹配失敗,將使用這個模式。這個規則規定“配一個任意的i32立即數,把它轉換為一個ORI指令(或一個16位立即數)及一個LIS指令(載入16位立即數,並且偏移左16位)”。要使這可行,使用LO16/HI16節點轉換來操作輸入的立即數(這時,獲取該立即數的高16位或低16位)。

·        在使用‘Pat’類把一個模式對映到具有一個或多個複雜運算元的一條指令(比如像X86的定址模式)時,該模式可以使用一個ComplexPattern來整體說明運算元,或者它也可以分開地說明覆雜運算元的組成。後者的實現有PowerPC後端的前置遞增指令:

def STWU : DForm_1<37, (outs ptr_rc:$ea_res), (ins GPRC:$rS, memri:$dst),
                "stwu $rS, $dst", LdStStoreUpd, []>,
                RegConstraint<"$dst.reg = $ea_res">, NoEncode<"$ea_res">;
def : Pat<(pre_store GPRC:$rS, ptr_rc:$ptrreg, iaddroff:$ptroff),
          (STWU GPRC:$rS, iaddroff:$ptroff, ptr_rc:$ptrreg)>;

這裡,ptroff與ptrreg這對運算元被匹配到STWU指令中型別為memri的複雜運算元dst身上。(作者注:STWU的“(ins GPRC:$rS, memri:$dst)”賦值給基類Instruction的InOperandList,而“(STWU GPRC:$rS,iaddroff:$ptroff, ptr_rc:$ptrreg)”則是所謂的結果模式,即匹配得到的指令。STWU後是輸入引數,即定義中的ins部分。其中memri是Operand的派生定義,在其定義裡MIOperandInfo = (opsdispRI:$imm, ptr_rc_nor0:$reg),即memri:$dst是包含兩個引數dispRI:$imm與ptr_rc_nor0:$reg的dag。引數的匹配關係是按位置一一對應,即dispRI:$imm匹配iaddroff:$ptroff,ptr_rc_nor0:$reg匹配ptr_rc:$ptrreg)。

·        儘管系統自動做了許多工作,如果存在難以表達的物件,它仍然允許你編寫定製的C++程式碼來匹配特殊的情形。

儘管擁有許多長處,該系統當前也有一些侷限,主要因為它仍在進行,尚未完成:

·        總體上,沒有辦法定義或匹配定義了多個值的SelectionDAG節點(比如SMUL_LOHI,LOAD,CALL等)。這是目前你仍然需要為你的指令選擇器編寫定製C++程式碼的原因。

·        還沒有好的方式來支援匹配複雜的取址模式。在未來,我們將擴充套件模式片段以允許它們定義多個值(比如X86取址模式中的四個運算元,目前它們由定製C++程式碼匹配)。另外,我們將擴充套件片段,使一個片段可以匹配多個不同的模式。

·        我們還不能自動地推導像isStore/isLoad這樣的標記。

·        還不能為Legalizer自動地產生支援的暫存器及操作的集合。

·        還沒有辦法把定製的與合法化的節點聯絡起來。

儘管有這些侷限,指令選擇器生成器對於典型指令集中大多數二元操作與邏輯操作仍然十分有用。如果你遇到任何問題,或不知道怎麼做,請告訴Chris(作者注:Chris Lattner,LLVM專案的創始人)!

1.2.            SelectionDAG

文件“The LLVM Target-Independent Code Generator”是這樣介紹SelectionDAG的:

SelectionDAG對程式碼表示提供了一個抽象,以經得起使用自動化技術的指令選擇(比如,基於模式匹配優化選擇器的動態規劃)檢驗的方式 。它還良好地適應了程式碼生成的其他階段;特別的,指令排程(SelectionDAG非常接近於選擇後DAG排程)。另外,SelectionDAG提供了一個宿主表示,在其中可以執行各式各樣、非常低階(但和目標機器無關)的優化;這要求關於目標機器高效支援指令的大量資訊。

SelectionDAG是一個有向無環圖,其節點是SDNode的派生例項。SDNode的主要載荷是表示該節點執行哪個操作的操作碼,以及運算元。在檔案include/llvm/CodeGen/SelectionDAGNodes.h的開頭描述了各種操作節點型別。

雖然大多數操作定義了單個值,在圖中的每個節點可能定義了多個值。例如,一個複合的div/rem操作同時定義了商與餘數。許多其他情形同樣要求多個值。每個節點還有若干運算元,它們是通向定義了這個使用值節點的邊。因為節點可能定義多個值,邊由類SDValue的例項表示,它是一對<SDNode, unsigned>,分別表示節點及要被使用的結果值。由一個SDNode產生的每個值具有一個關聯的MVT(機器值型別,Machine Value Type),以表示該值的型別。

SelectionDAG包含兩種型別的值:表示資料流的,以及表示控制流依賴性的。資料值是具有一個整數或浮點值型別的簡單邊。控制邊被表示作“鏈(chain)”邊,具有型別MVT::Other。這些邊在具有副作用的節點間(比如load,store,call,return等)提供了一個次序。所有具有副作用的節點接受一個符號鏈作為輸入,併產生一個新的符號鏈作為輸出。按照慣例,符號鏈輸入總是運算元0,而一個操作產生的最後的值總是鏈結果。不過,在指令選擇後,在機器節點(指MemSDNode)中鏈在指令運算元後,後面可能跟著黏結節點。

一個SelectionDAG具有指定的“入口,Entry”及“根,Root”節點。入口節點總是一個具有操作碼ISD::EntryToken的標記節點。根節點是符號鏈中最終副作用節點。例如,在一個單個基本塊函式中,它就是返回節點。

SelectionDAG的一個重要的概念是“合法”與“非法”觀念。目標機器合法的DAG僅使用支援的操作以及支援的型別。例如在一臺32位PowerPC上,具有型別i1,i8,i16或i64型別值的DAG是非法的,使用SREM或UREM操作的DAG也是。型別與操作合法化階段負責把非法的DAG轉換為合法的DAG。

基於SelectionDAG的指令選擇包含以下步驟:

1.     構建初始DAG——這個步驟執行從輸入的LLVM程式碼到一個非法SelectionDAG的簡單轉換。

2.     優化SelectionDAG——這個步驟在SelectionDAG上執行簡單的優化以簡化之,併為支援元操作的目標機器識別元指令(像rotate及div/rem對)。這使得結果程式碼更加高效,並使得從DAG選擇指令更簡單。

3.     合法化SelectionDAG型別——這個步驟消除任何目標機器不支援的型別。

4.     優化SelectionDAG——執行SelectionDAG優化器以清除型別合法化造成的重複。

5.     合法化SelectionDAG操作——這個步驟消除任何目標機器不支援的操作。

6.     優化SelectionDAG——執行SelectionDAG優化器以清除操作合法化造成的重複。

7.     從DAG選擇指令——最後,目標機器指令選擇器匹配DAG操作到目標機器指令。這個過程把目標機器無關的DAG輸入翻譯到另一個目標機器指令的DAG。

8.     SelectionDAG排程與編隊——最後這個階段向目標機器指令DAG中的指令賦予一個線性次序,並把它們送入正在編譯的MachineFunction。這個步驟使用傳統的prepass scheduling技術。

在所有這些步驟完成後,SelectionDAG被摧毀,執行餘下的程式碼生成遍。

在這一篇文件裡是這樣說SelectionDAG的來由:

初始的SelectionDAG是SelectionDAGBuilder類從LLVM輸入由輕信的窺孔展開得到。這個遍的目的是向SelectionDAG儘可能多地展露低階、目標機器特定的細節。這個遍幾乎是完全寫死的(比如一個LLVM add轉換為一個SDNode add,而一個getelementptr展開為平淡無奇的算術)。這個遍要求目標機器特定的鉤子來降級“呼叫”、“返回”、“變長引數列表”等。對這些特性,使用TargetLowering介面。