前言
網(wǎng)上關(guān)于編譯優(yōu)化的文章很多,但大多零零散散,不成體系,本文試圖給出一個(gè)完整和清晰的優(yōu)化思路,同時(shí)提供在實(shí)踐中如何進(jìn)行優(yōu)化的詳盡參考。但是,在介紹所有優(yōu)化知識(shí)之前首先引用LFS-Book中的一句忠告:“使用編譯器優(yōu)化得到的小幅度性能提升,與它帶來(lái)的風(fēng)險(xiǎn)相比微不足道”。你還要進(jìn)行優(yōu)化嗎?
?
OK, crazy guy! Let's Go!!
在繼續(xù)之前,作者還是奉勸各位:如果追求極致的優(yōu)化,那么它將是一件既耗時(shí)又麻煩的事情,你會(huì)陷入無(wú)止盡的測(cè)試、測(cè)試、再測(cè)試……另外 Gentoo wiki 上有這么一句話:"GCC has well over a hundred individual optimization flags and it would be insane to try and describe them all."所以本文不會(huì)涉及全部GCC優(yōu)化選項(xiàng)。最后作者還是再羅唆一句:優(yōu)化應(yīng)當(dāng)適可而止為好,將精力留出來(lái)做一些其它事情會(huì)更有意義!
先決條件
本文的主要讀者是 LFS/Gentoo 的玩家,基本上比較 crazy 的玩家都接觸過(guò),如果你之前從未使用過(guò) LFS/Gentoo ,請(qǐng)先按照《Linux From Scratch 6.2 中文版》做一遍 LFS ,然后再來(lái)閱讀此文將會(huì)更有意義。另外,本文是建立在《深入理解軟件包的配置、編譯與安裝》一文基礎(chǔ)之上的,在開始閱讀本文之前,請(qǐng)先閱讀它。
基本原理
我們首先從三個(gè)方面來(lái)看與優(yōu)化相關(guān)的內(nèi)容:
從運(yùn)行時(shí)的依賴關(guān)系來(lái)看,對(duì)性能有較大影響的組件有 kernel 和 glibc ,雖然這嚴(yán)格說(shuō)來(lái)這不屬于本文的話題,但是經(jīng)過(guò)精心選擇、精心配置、精心編譯的內(nèi)核與C庫(kù)將對(duì)提高系統(tǒng)的運(yùn)行速度起著基礎(chǔ)性的作用。
從被編譯的軟件包來(lái)看,每個(gè)軟件包的 configure 腳本都提供了許多配置選項(xiàng),其中有許多選項(xiàng)是與性能息息相關(guān)的。比如,對(duì)于 Apache-2.2.6 而言,你可以使用 --enable-MODULE=static 將模塊靜態(tài)編譯進(jìn)核心,使用 --disable-MODULE 禁用不需要的模塊,使用 --with-mpm=MPM 選擇一個(gè)高效的多路處理模塊,在不需要IPv6的情況下使用 --disable-ipv6 禁用IPv6支持,在不使用線程化的MPM時(shí)使用 --disable-threads 禁用線程支持,等等……這部分內(nèi)容顯然不可能在本文中進(jìn)行完整的講述,本文只能講述與優(yōu)化相關(guān)的通用選項(xiàng)。針對(duì)特定的軟件包,請(qǐng)?jiān)诰幾g前使用 configure --help 查看所有選項(xiàng),并精心選擇。
從編譯過(guò)程自身來(lái)看,將源代碼編譯為二進(jìn)制文件是在 Makefile 文件的指導(dǎo)下,由 make 程序調(diào)用一條條編譯命令完成的。而將源代碼編譯為二進(jìn)制文件又需要經(jīng)過(guò)以下四個(gè)步驟:預(yù)處理(cpp) → 編譯(gcc或g++) → 匯編(as) → 連接(ld) ;括號(hào)中表示每個(gè)階段所使用的程序,它們分別屬于 GCC 和 Binutils 軟件包。顯然的,優(yōu)化應(yīng)當(dāng)從編譯工具自身的選擇以及控制編譯工具的行為入手。
大體上編譯優(yōu)化就這"三板斧"(其實(shí)是"三腳貓")了,本文接下來(lái)的內(nèi)容將討論這只貓的后兩只腳。
編譯工具的選擇
對(duì)于編譯工具自身的選擇,在假定使用 Binutils 和 GCC 以及 Make 的前提下,沒什么好說(shuō)的,基本上新版本都能帶來(lái)性能提升,同時(shí)比老版本對(duì)新硬件的支持更好,所以應(yīng)當(dāng)盡量選用新版本。不過(guò)追新也可能帶來(lái)系統(tǒng)的不穩(wěn)定,這就要針對(duì)實(shí)際情況進(jìn)行權(quán)衡了。本文以 Binutils-2.18 和 GCC-4.2.2/GCC-4.3.0 以及 Make-3.81 為例進(jìn)行說(shuō)明。
configure 選項(xiàng)
這里我們只講解通用的"體系結(jié)構(gòu)選項(xiàng)",由于"特性選項(xiàng)"在每個(gè)軟件包之間千差萬(wàn)別,所以不可能在此處進(jìn)行講解。
這部分內(nèi)容很簡(jiǎn)單,并且其含義也是不言而喻的,下面只列出常用的值:
i586-pc-linux-gnu
i686-pc-linux-gnu
x86_64-pc-linux-gnu
powerpc-unknown-linux-gnu
powerpc64-unknown-linux-gnu
如果你實(shí)在不知道應(yīng)當(dāng)使用哪一個(gè),那么就干脆不使用這幾個(gè)選項(xiàng),讓 config.guess 腳本自己去猜吧,反正也挺準(zhǔn)的。
編譯選項(xiàng)
讓我們先看看 Makefile 規(guī)則中的編譯命令通常是怎么寫的。
大多數(shù)軟件包遵守如下約定俗成的規(guī)范:
#1,首先從源代碼生成目標(biāo)文件(預(yù)處理,編譯,匯編),"-c"選項(xiàng)表示不執(zhí)行鏈接步驟。
$(CC) $(CPPFLAGS) $(CFLAGS) example.c?? -c?? -o example.o
#2,然后將目標(biāo)文件連接為最終的結(jié)果(連接),"-o"選項(xiàng)用于指定輸出文件的名字。
$(CC) $(LDFLAGS) example.o?? -o example
#有一些軟件包一次完成四個(gè)步驟:
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) example.c?? -o example
當(dāng)然也有少數(shù)軟件包不遵守這些約定俗成的規(guī)范,比如:
#1,有些在命令行中漏掉應(yīng)有的Makefile變量(注意:有些遺漏是故意的)
$(CC) $(CFLAGS) example.c??? -c?? -o example.o
$(CC) $(CPPFLAGS) example.c? -c?? -o example.o
$(CC) example.o?? -o example
$(CC) example.c?? -o example
#2,有些在命令行中增加了不必要的Makefile變量
$(CC) $(CFLAGS) $(LDFLAGS) example.o?? -o example
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) example.c?? -c?? -o example.o
當(dāng)然還有極個(gè)別軟件包完全是"胡來(lái)":亂用變量(增加不必要的又漏掉了應(yīng)有的)者有之,不用$(CC)者有之,不一而足.....
盡管將源代碼編譯為二進(jìn)制文件的四個(gè)步驟由不同的程序(cpp,gcc/g++,as,ld)完成,但是事實(shí)上 cpp, as, ld 都是由 gcc/g++ 進(jìn)行間接調(diào)用的。換句話說(shuō),控制了 gcc/g++ 就等于控制了所有四個(gè)步驟。從 Makefile 規(guī)則中的編譯命令可以看出,編譯工具的行為全靠 CC/CXX CPPFLAGS CFLAGS/CXXFLAGS LDFLAGS 這幾個(gè)變量在控制。當(dāng)然理論上控制編譯工具行為的還應(yīng)當(dāng)有 AS ASFLAGS ARFLAGS 等變量,但是實(shí)踐中基本上沒有軟件包使用它們。
那么我們?nèi)绾慰刂七@些變量呢?一種簡(jiǎn)易的做法是首先設(shè)置與這些 Makefile 變量同名的環(huán)境變量并將它們 export 為全局,然后運(yùn)行 configure 腳本,大多數(shù) configure 腳本會(huì)使用這同名的環(huán)境變量代替 Makefile 中的值。但是少數(shù) configure 腳本并不這樣做(比如GCC-3.4.6和Binutils-2.16.1的腳本就不傳遞LDFLAGS),你必須手動(dòng)編輯生成的 Makefile 文件,在其中尋找這些變量并修改它們的值,許多源碼包在每個(gè)子文件夾中都有 Makefile 文件,真是一件很累人的事!
CC 與 CXX
這是 C 與 C++ 編譯器命令。默認(rèn)值一般是 "gcc" 與 "g++"。這個(gè)變量本來(lái)與優(yōu)化沒有關(guān)系,但是有些人因?yàn)閾?dān)心軟件包不遵守那些約定俗成的規(guī)范,害怕自己苦心設(shè)置的 CFLAGS/CXXFLAGS/LDFLAGS 之類的變量被忽略了,而索性將原本應(yīng)當(dāng)放置在其它變量中的選項(xiàng)一股老兒塞到 CC 或 CXX 中,比如:CC="gcc -march=k8 -O2 -s"。這是一種怪異的用法,本文不提倡這種做法,而是提倡按照變量本來(lái)的含義使用變量。
CPPFLAGS
這是用于預(yù)處理階段的選項(xiàng)。不過(guò)能夠用于此變量的選項(xiàng),看不出有哪個(gè)與優(yōu)化相關(guān)。如果你實(shí)在想設(shè)一個(gè),那就使用下面這兩個(gè)吧:
-DNDEBUG
"NDEBUG"是一個(gè)標(biāo)準(zhǔn)的 ANSI 宏,表示不進(jìn)行調(diào)試編譯。
-D_FILE_OFFSET_BITS=64
大多數(shù)包使用這個(gè)來(lái)提供大文件(>2G)支持。
CFLAGS 與 CXXFLAGS
CFLAGS 表示用于 C 編譯器的選項(xiàng),CXXFLAGS 表示用于 C++ 編譯器的選項(xiàng)。這兩個(gè)變量實(shí)際上涵蓋了編譯和匯編兩個(gè)步驟。大多數(shù)程序和庫(kù)在編譯時(shí)默認(rèn)的優(yōu)化級(jí)別是"2"(使用"-O2"選項(xiàng))并且?guī)в姓{(diào)試符號(hào)來(lái)編譯,也就是 CFLAGS="-O2 -g", CXXFLAGS=$CFLAGS 。事實(shí)上,"-O2"已經(jīng)啟用絕大多數(shù)安全的優(yōu)化選項(xiàng)了。另一方面,由于大部分選項(xiàng)可以同時(shí)用于這兩個(gè)變量,所以僅在最后講述只能用于其中一個(gè)變量的選項(xiàng)。[提醒]下面所列選項(xiàng)皆為非默認(rèn)選項(xiàng),你只要按需添加即可。
先說(shuō)說(shuō)"-O3"在"-O2"基礎(chǔ)上增加的幾項(xiàng):
-finline-functions
允許編譯器選擇某些簡(jiǎn)單的函數(shù)在其被調(diào)用處展開,比較安全的選項(xiàng),特別是在CPU二級(jí)緩存較大時(shí)建議使用。
-funswitch-loops
將循環(huán)體中不改變值的變量移動(dòng)到循環(huán)體之外。
-fgcse-after-reload
為了清除多余的溢出,在重載之后執(zhí)行一個(gè)額外的載入消除步驟。
另外:
-fomit-frame-pointer
對(duì)于不需要棧指針的函數(shù)就不在寄存器中保存指針,因此可以忽略存儲(chǔ)和檢索地址的代碼,同時(shí)對(duì)許多函數(shù)提供一個(gè)額外的寄存器。所有"-O"級(jí)別都打開它,但僅在調(diào)試器可以不依靠棧指針運(yùn)行時(shí)才有效。在AMD64平臺(tái)上此選項(xiàng)默認(rèn)打開,但是在x86平臺(tái)上則默認(rèn)關(guān)閉。建議顯式的設(shè)置它。
-falign-functions=N
-falign-jumps=N
-falign-loops=N
-falign-labels=N
這四個(gè)對(duì)齊選項(xiàng)在"-O2"中打開,其中的根據(jù)不同的平臺(tái)N使用不同的默認(rèn)值。如果你想指定不同于默認(rèn)值的N,也可以單獨(dú)指定。比如,對(duì)于L2-cache>=1M的cpu而言,指定 -falign-functions=64 可能會(huì)獲得更好的性能。建議在指定了 -march 的時(shí)候不明確指定這里的值。
調(diào)試選項(xiàng):
-fprofile-arcs
在使用這一選項(xiàng)編譯程序并運(yùn)行它以創(chuàng)建包含每個(gè)代碼塊的執(zhí)行次數(shù)的文件后,程序可以再次使用 -fbranch-probabilities 編譯,文件中的信息可以用來(lái)優(yōu)化那些經(jīng)常選取的分支。如果沒有這些信息,gcc將猜測(cè)哪個(gè)分支將被經(jīng)常運(yùn)行以進(jìn)行優(yōu)化。這類優(yōu)化信息將會(huì)存放在一個(gè)以源文件為名字的并以".da"為后綴的文件中。
全局選項(xiàng):
-pipe
在編譯過(guò)程的不同階段之間使用管道而非臨時(shí)文件進(jìn)行通信,可以加快編譯速度。建議使用。
目錄選項(xiàng):
--sysroot=dir
將dir作為邏輯根目錄。比如編譯器通常會(huì)在 /usr/include 和 /usr/lib 中搜索頭文件和庫(kù),使用這個(gè)選項(xiàng)后將在 dir/usr/include 和 dir/usr/lib 目錄中搜索。如果使用這個(gè)選項(xiàng)的同時(shí)又使用了 -isysroot 選項(xiàng),則此選項(xiàng)僅作用于庫(kù)文件的搜索路徑,而 -isysroot 選項(xiàng)將作用于頭文件的搜索路徑。這個(gè)選項(xiàng)與優(yōu)化無(wú)關(guān),但是在 CLFS 中有著神奇的作用。
代碼生成選項(xiàng):
-fno-bounds-check
關(guān)閉所有對(duì)數(shù)組訪問(wèn)的邊界檢查。該選項(xiàng)將提高數(shù)組索引的性能,但當(dāng)超出數(shù)組邊界時(shí),可能會(huì)造成不可接受的行為。
-freg-struct-return
如果struct和union足夠小就通過(guò)寄存器返回,這將提高較小結(jié)構(gòu)的效率。如果不夠小,無(wú)法容納在一個(gè)寄存器中,將使用內(nèi)存返回。建議僅在完全使用GCC編譯的系統(tǒng)上才使用。
-fpic
生成可用于共享庫(kù)的位置獨(dú)立代碼。所有的內(nèi)部尋址均通過(guò)全局偏移表完成。要確定一個(gè)地址,需要將代碼自身的內(nèi)存位置作為表中一項(xiàng)插入。該選項(xiàng)產(chǎn)生可以在共享庫(kù)中存放并從中加載的目標(biāo)模塊。
-fstack-check
為防止程序棧溢出而進(jìn)行必要的檢測(cè),僅在多線程環(huán)境中運(yùn)行時(shí)才可能需要它。
-fvisibility=hidden
設(shè)置默認(rèn)的ELF鏡像中符號(hào)的可見性為隱藏。使用這個(gè)特性可以非常充分的提高連接和加載共享庫(kù)的性能,生成更加優(yōu)化的代碼,提供近乎完美的API輸出和防止符號(hào)碰撞。我們強(qiáng)烈建議你在編譯任何共享庫(kù)的時(shí)候使用該選項(xiàng)。參見 -fvisibility-inlines-hidden 選項(xiàng)。
硬件體系結(jié)構(gòu)相關(guān)選項(xiàng)[僅僅針對(duì)x86與x86_64]:
-march=cpu-type
為特定的cpu-type編譯二進(jìn)制代碼(不能在更低級(jí)別的cpu上運(yùn)行)。Intel可以用:pentium2, pentium3(=pentium3m), pentium4(=pentium4m), pentium-m, prescott, nocona, core2(GCC-4.3新增) 。AMD可以用:k6-2(=k6-3), athlon(=athlon-tbird), athlon-xp(=athlon-mp), k8(=opteron=athlon64=athlon-fx)
-mfpmath=sse
P3和athlon-xp級(jí)別及以上的cpu支持"sse"標(biāo)量浮點(diǎn)指令。僅建議在P4和K8以上級(jí)別的處理器上使用該選項(xiàng)。
評(píng)論
查看更多