自編教材分享:第五章—編譯與運行優(yōu)化(三)


目標代碼生成
該過程是把中間代碼變換成特定機器上的目標代碼,形式上包括:絕對指令代碼、可重定位的指令代碼、匯編指令代碼。這是編譯的最后階段,它的工作與硬件系統(tǒng)結構和指令含義有關,涉及到硬件系統(tǒng)功能部件的運用、機器指令的選擇、各種數(shù)據(jù)類型變量的存儲空間分配以及寄存器分配等。在LLVM中使用clang -S file.c命令進行編譯,生成特定平臺的匯編代碼.s文件。
以匯編文件中的加法指令為例,該匯編指令由一個操作碼和兩個操作數(shù)組成,操作碼addl為加法操作,操作數(shù)%x和操作數(shù)%y執(zhí)行加法運算,并將結果放置在%x中。

file.c文件:
file.ll文件:
file.s文件:
目標文件格式
目標文件是源代碼編譯后但未鏈接的中間文件,它跟可執(zhí)行文件的內(nèi)容與結構很相似,從廣義上看目標文件和可執(zhí)行文件的格式幾乎是一樣的,在Linux操作系統(tǒng)下都是按照可執(zhí)行可鏈接文件格式(Executable Linkable Format,ELF)進行存儲。 ELF目標文件格式的最前部是ELF頭,包含描述整個文件的基本屬性,包括ELF文件版本、目標機器型號、程序入口地址等。節(jié)頭表描述目標文件中各個節(jié)的信息,在ELF頭和節(jié)頭表之間是節(jié)本身,其中包含十個段。這十段內(nèi)容組成了可重定位目標文件,有了這些目標文件之后,通過靜態(tài)鏈接就可以將它們組合起來,形成一個可以運行的程序。

總體來說,程序源代碼被編譯以后,按照代碼和數(shù)據(jù)分別存放到相應的段中,代碼段.text段屬于程序指令,.data段和.bss段屬于程序數(shù)據(jù),編譯器或匯編器還會將一些輔助性信息諸如符號、重定位信息等也按照表的方式存放在目標文件中,通常情況下,一個表往往是一個段。
相關選項
源于LLVM的設計是高度模塊化的,所以針對后端會有特定的優(yōu)化,比如X86、ARM后端等,并通過優(yōu)化選項控制。例如針對X86結構的常用后端選項-mavx,功能是支持MMX、SSE和AVX等內(nèi)置函數(shù)和代碼的生成。在代碼生成階段可以通過數(shù)據(jù)選項選擇對數(shù)據(jù)的處理方式,通過目標平臺選項生成指定平臺的代碼,通過后端選項打開不同后端支持的優(yōu)化功能。
數(shù)據(jù)選項用于控制數(shù)據(jù)相關操作,包括數(shù)據(jù)對齊的方式、強制double輸入類型的位數(shù)、內(nèi)存模型的設定等,比如常用選項-mdouble=<value>的功能是指定double類型數(shù)據(jù)的位數(shù),其中參數(shù)可設置為32和64。
通過目標平臺選項可以指定生成代碼的目標運行機器,包括X86、AMDGPU、ARM等,比如-march=<cpu>選項,指定LLVM為特定的處理器生成代碼。

匯編與鏈接
匯編器
匯編器是將匯編代碼轉變?yōu)闄C器可以執(zhí)行的指令,每一個匯編語句都對應一條機器指令,由.s匯編文件經(jīng)匯編器生成.o目標代碼文件。
若有變量定義在其它目標代碼文件中,則只有運行鏈接的時候才能確定絕對地址,所以現(xiàn)代的編譯器可以將一個源代碼文件編譯成一個可重定位的目標文件,最終由鏈接器將這些目標文件鏈接起來形成可執(zhí)行文件。
鏈接的功能是將一個或多個目標文件以及庫文件合并為一個可執(zhí)行文件。鏈接可以在源代碼翻譯成機器代碼即編譯的時候完成,也可以在程序裝入內(nèi)存時完成,甚至可以在程序運行時完成,根據(jù)不同的完成時期可將鏈接分為靜態(tài)鏈接和動態(tài)鏈接。
靜態(tài)鏈接與動態(tài)鏈接
靜態(tài)鏈接
靜態(tài)鏈接指鏈接器將外部函數(shù)所在的靜態(tài)鏈接庫直接拷貝到目標可執(zhí)行程序中,這樣在執(zhí)行該程序時這些代碼會被裝入到該進程的虛擬地址空間中。
關于靜態(tài)鏈接,首先使用clang將a.c和b.c分別編譯成目標文件a.o和b.o。從代碼中可以看到,b.c總共定義了兩個全局符號,一個是變量shared,另外一個是函數(shù)add。 程序a.c里面定義了一個全局符號是main,且引用b.c里面的shared和add,接下來就是把a.o和b.o這兩個目標文件鏈接在一起并最終形成一個可執(zhí)行文件ab.out。
動態(tài)鏈接
動態(tài)鏈接是把程序拆分成各個相對獨立部分,在程序運行時才將它們鏈接在一起形成一個完整的程序。
動態(tài)鏈接不是像靜態(tài)鏈接一樣把所有的源代碼文件都鏈接成一個單獨的可執(zhí)行文件。它首先將hello.c編譯生成hello.o文件,由hello.o文件生成動態(tài)庫libhello.so文件,該文件是包含hello.c中hello()函數(shù)的共享對象文件,在編譯main.c時通過鏈接動態(tài)庫libhello.so文件生成可執(zhí)行文件。
動態(tài)鏈接與靜態(tài)鏈接的對比
靜態(tài)鏈接對目標文件的更新很不友好,假如一個.o目標文件依賴20個.o目標文件,當20個目標文件中有一個需要更新時,需要將所有目標文件的源代碼重新編譯出一個可執(zhí)行程序才可以更新成功,為了解決靜態(tài)鏈接的這一缺點,所以引入了動態(tài)鏈接。動態(tài)鏈接可以壓縮可執(zhí)行文件的大小,缺點是可移植性太差,如果兩臺機器運行環(huán)境不同,動態(tài)庫存放的位置不一致,可能會導致程序運行失敗。總結來說,靜態(tài)鏈接和動態(tài)鏈接各有優(yōu)點和缺點。優(yōu)化人員可根據(jù)實際需要在兩種方式之間選擇,挑選出最適合的鏈接方式,以達到最優(yōu)的效果。

鏈接時優(yōu)化
鏈接時優(yōu)化是鏈接期間的程序優(yōu)化,多個中間文件通過鏈接器合并在一起組合為一個程序,縮減代碼體積,并通過對整個程序的分析以實現(xiàn)更好的運行時性能。優(yōu)化人員通過選項-flto指示LLVM編譯器生成含有LLVM比特碼的.o文件,將代碼生成延遲到鏈接階段,并在鏈接階段對代碼實現(xiàn)進一步地優(yōu)化。

當鏈接器檢測到.o文件為LLVM比特碼時,會將所有的比特碼文件讀入內(nèi)存并鏈接起來,然后再進行跨文件地內(nèi)聯(lián)、常量傳播和更激進地死代碼消除等優(yōu)化。
選項-flto后可跟參數(shù)。默認情況下-flto為full模式。-flto=full指鏈接時優(yōu)化將分散的目標文件的LLVM IR組合到一個大的LLVM目標文件中,然后對其整體分析、優(yōu)化并生成機器碼。-flto=thin是把目標文件分開,根據(jù)需要才從其它目標文件中導入功能,使用選項-flto=thin鏈接的速度要快于使用選項-flto=full。
在程序的編譯階段,LLVM編譯器通過建立全局函數(shù)調用圖從而發(fā)現(xiàn)并刪除沒有被調用的死函數(shù)。在程序的鏈接階段,鏈接器對所有的輸入文件進行解析后,可以建立符號以及函數(shù)的相互引用關系,從而發(fā)現(xiàn)并刪除沒有被引用的符號以及對應的函數(shù)。所以LLVM編譯器和鏈接器之間的緊密集成實現(xiàn)了更多地優(yōu)化。
示例:

LLVM將輸入的源文件a.c編譯成LLVM比特碼文件,將輸入的源文件main.c編譯成本機目標代碼。#a.o 是LLVM比特碼文件;#main.o是本機目標代碼文件
最后通過命令將兩個文件鏈接生成可執(zhí)行文件main,在該過程中,鏈接器首先識別出a.c中的foo2()是LLVM 比特碼文件中定義的外部可見符號,完成通常的符號解析傳遞,發(fā)現(xiàn)foo2()并沒有被使用后,LLVM編譯器刪除foo2()。一旦刪除了foo2(),編譯器會識別出條件i=0,這表明foo3()從來沒被使用過,因此刪除foo3(),之后刪除foo4(),最后生成的文件如右邊所示。這個例子說明了編譯器與鏈接器緊密集成的優(yōu)點,編譯器不能在沒有鏈接器輸入的情況下刪除foo3()。
數(shù)學庫
程序在編譯過程中經(jīng)常鏈接數(shù)學庫,數(shù)學庫是開展科學計算、工程計算等必備的核心基礎軟件,數(shù)學函數(shù)的性能、可靠性和精度對上層應用程序尤其是科學計算程序的解算至關重要。使用數(shù)學庫中提供的各類數(shù)學函數(shù),能夠縮短應用程序的開發(fā)周期,并獲取庫函數(shù)所帶來的性能收益。
除了前文提到的基礎數(shù)學函數(shù)庫之外,還有很多擴展數(shù)學庫,如BLAS庫、LAPACK庫、MKL庫等,這些數(shù)學庫廣泛應用于人工智能、數(shù)據(jù)分析等科學計算領域,其中BLAS庫已經(jīng)成為初等線性代數(shù)運算的業(yè)界標準,被廣泛應用于科學及工程計算,也是許多數(shù)學軟件的基本核心,本節(jié)以BLAS庫為例介紹數(shù)學庫的使用。
BLAS(Basic Linear Algebra Subprograms)基本線性代數(shù)庫是一組高質量的基本向量、矩陣運算子程序。由于BLAS涉及最基本的向量、矩陣運算應用程序的開發(fā)者只需要運用適當?shù)募夹g將計算過程抽象為矩陣、向量的基本運算,就可以調用相應的BLAS庫函數(shù)而不必考慮與計算機體系結構相關的性能優(yōu)化問題。
BLAS從結構上分為三部分包括:
Level 1 BLAS:向量和向量,向量和標量之間的運算;
Level 2 BLAS:向量和矩陣間的運算;
Level 3 BLAS:矩陣和矩陣之間的運算。
BLAS庫函數(shù)的命名由三部分組成:數(shù)據(jù)類型、矩陣類型、操作類型。
BLAS庫支持的數(shù)據(jù)類型

BLAS庫支持的矩陣類型

BLAS庫支持的常用函數(shù)操作

英特爾數(shù)學內(nèi)核庫(Intel Math Kernel Library,MKL)為英特爾包含有BLAS計算功能的數(shù)學核心函數(shù)庫。當在C語言中使用該數(shù)學庫編程時需引用頭文件mkl_cblas.h。
使用Intel 的ICC編譯器對代碼進行編譯,需要添加選項-mkl=<arg>選項,其中不同的參數(shù)表示的含義不同,包括:
?-mkl或-mkl=parallel:并行鏈接Intel(R) MKL庫,這也是使用-mkl選項時的默認值;
?-mkl=sequential:采用串行Intel(R) MKL庫鏈接;
?-mkl=cluster:使用Intel(R) MKL Cluster庫和Intel(R) MKL序列庫鏈接。
數(shù)學庫優(yōu)化
在性能要求苛刻的情況下,函數(shù)庫提供的性能有時并不能滿足優(yōu)化人員的需要,因此需要優(yōu)化人員提升庫的性能。優(yōu)化人員可以根據(jù)一些平臺無關的算法,在考慮平臺的相關特性后自行對庫函數(shù)進行優(yōu)化,以達到對于性能的要求。
通常代碼中的abs數(shù)學函數(shù)都是標量運算,本示例在沒有數(shù)據(jù)依賴的情況下,將標量abs運算改為向量abs運算,這樣可以同時處理4個abs值求解。
未向量化示例abs.c:
手工向量化示例abs-vec.c:
相關選項
匯編與鏈接相關選項如下表,其中選項-flto=full指鏈接時優(yōu)化將分散的目標文件的LLVM IR組合到一個大的LLVM目標文件中,然后對其整體分析、優(yōu)化并生成機器碼。-flto=thin是把目標文件分開,根據(jù)需要才從其它目標文件中導入功能,使用選項-flto=thin鏈接的速度要快于使用選項-flto=full。
