1. 程式人生 > >gcc -O0 -O1 -O2 -O3 四級優化選項及每級分別做什麼優化

gcc -O0 -O1 -O2 -O3 四級優化選項及每級分別做什麼優化

今天看到了一篇文章,寫的挺好就將其轉載,

Gcc 編譯優化簡介 gcc 提供了為了滿足使用者不同程度的的優化需要,提供了近百種優化選項,用來對{編譯時間,目標檔案長度,執行效率}這個三維模型進行不同的取捨和平衡。優化的方法不一而足,總體上將有以下幾類:1)精簡操作指令;2)儘量滿足cpu的流水操作;3)通過對程式行為地猜測,重新調整程式碼的執行順序;4)充分使用暫存器;5)對簡單的呼叫進行展開等等。想全部瞭解這些編譯選項,並在其中挑選適合的選項進行優化,無疑像個噩夢般的過程。單從gnu的官方網站上得到的手冊來看,描述依然比較蒼白,不足以完全瞭解選項的使用範圍和原理。(GCC has well over a hundred individual optimization flags and it would be insane to try and describe them all) 

幸而gcc提供了從O0-O3以及Os這幾種不同的優化級別供大家選擇,在這些選項中,包含了大部分有效的編譯優化選項,並且可以在這個基礎上,對某些選項進行遮蔽或新增,從而大大降低了使用的難度,畢竟,在一定基礎上進行取捨,比萬事從頭開始要好得多。下面著重圍繞這幾個不同的級別進行簡單介紹。(由於gcc不同版本手冊差異比較大,以下主要以gcc-3.4.6為參考) 

-O0: 不做任何優化,這是預設的編譯選項。 

-O和-O1: 對程式做部分編譯優化,對於大函式,優化編譯佔用稍微多的時間和相當大的記憶體。使用本項優化,編譯器會嘗試減小生成程式碼的尺寸,以及縮短執行時間,但並不執行需要佔用大量編譯時間的優化。 

開啟的優化選項: 

l -fdefer-pop:延遲棧的彈出時間。當完成一個函式呼叫,引數並不馬上從棧中彈出,而是在多個函式被呼叫後,一次性彈出。 

l -fmerge-constants:嘗試橫跨編譯單元合併同樣的常量(string constants and floating point constants) 

l -fthread-jumps:如果某個跳轉分支的目的地存在另一個條件比較,而且該條件比較包含在前一個比較語句之內,那麼執行本項優化.根據條件是true或者false,前面那條分支重定向到第二條分支的目的地或者緊跟在第二條分支後面. 

l -floop-optimize:執行迴圈優化,將常量表達式從迴圈中移除,簡化判斷迴圈的條件,並且optionally do strength-reduction,或者將迴圈開啟等。在大型複雜的迴圈中,這種優化比較顯著。 

l -fif-conversion:嘗試將條件跳轉轉換為等價的無分支型式。優化實現方式包括條件移動,min,max,設定標誌,以及abs指令,以及一些算術技巧等。  

l -fif-conversion2基本意義相同,沒有找到更多的解釋。 

l -fdelayed-branch:這種技術試圖根據指令週期時間重新安排指令。 它還試圖把儘可能多的指令移動到條件分支前, 以便最充分的利用處理器的治理快取。 

l -fguess-branch-probability:當沒有可用的profiling feedback或__builtin_expect時,編譯器採用隨機模式猜測分支被執行的可能性,並移動對應彙編程式碼的位置,這有可能導致不同的編譯器會編譯出迥然不同的目的碼。 

l -fcprop-registers:因為在函式中把暫存器分配給變數, 所以編譯器執行第二次檢查以便減少排程依賴性(兩個段要求使用相同的暫存器)並且刪除不必要的暫存器複製操作。 

-O2: 是比O1更高階的選項,進行更多的優化。Gcc將執行幾乎所有的不包含時間和空間折中的優化。當設定O2選項時,編譯器並不進行迴圈開啟()loop unrolling以及函式內聯。與O1比較而言,O2優化增加了編譯時間的基礎上,提高了生成程式碼的執行效率。 

O2開啟所有的O1選項,並開啟以下選項: 

l -fforce-mem:在做算術操作前,強制將記憶體資料copy到暫存器中以後再執行。這會使所有的記憶體引用潛在的共同表示式,進而產出更高效的程式碼,當沒有共同的子表示式時,指令合併將排出個別的暫存器載入。這種優化對於只涉及單一指令的變數, 這樣也許不會有很大的優化效果. 但是對於再很多指令(必須數學操作)中都涉及到的變數來說, 這會時很顯著的優化, 因為和訪問記憶體中的值相比 ,處理器訪問暫存器中的值要快的多。 

l -foptimize-sibling-calls:優化相關的以及末尾遞迴的呼叫。通常, 遞迴的函式呼叫可以被展開為一系列一般的指令, 而不是使用分支。 這樣處理器的指令快取能夠載入展開的指令並且處理他們, 和指令保持為需要分支操作的單獨函式呼叫相比, 這樣更快。 

l -fstrength-reduce:這種優化技術對迴圈執行優化並且刪除迭代變數。 迭代變數是捆綁到迴圈計數器的變數, 比如使用變數, 然後使用迴圈計數器變數執行數學操作的for-next迴圈。 

l -fcse-follow-jumps:在公用子表示式消元時,當目標跳轉不會被其他路徑可達,則掃描整個的跳轉表示式。例如,當公用子表示式消元時遇到if...else...語句時,當條為false時,那麼公用子表示式消元會跟隨著跳轉。   

l -fcse-skip-blocks:與-fcse-follow-jumps類似,不同的是,根據特定條件,跟隨著cse跳轉的會是整個的blocks 

l -frerun-cse-after-loop:在迴圈優化完成後,重新進行公用子表示式消元操作。 

l -frerun-loop-opt:兩次執行迴圈優化 l -fgcse:執行全域性公用子表示式消除pass。這個pass還執行全域性常量和copy propagation。這些優化操作試圖分析生成的組合語言程式碼並且結合通用片段, 消除冗餘的程式碼段。如果程式碼使用計算性的goto, gcc指令推薦使用-fno-gcse選項。 

l-fgcse-lm:全域性公用子表示式消除將試圖移動那些僅僅被自身儲存kill的裝載操作的位置。這將允許將迴圈內的load/store操作序列中的load轉移到迴圈的外面(只需要裝載一次),而在迴圈內改變成copy/store序列。在選中-fgcse後,預設開啟。 

l -fgcse-sm:當一個儲存操作pass在一個全域性公用子表示式消除的後面,這個pass將試圖將store操作轉移到迴圈外面去。如果與-fgcse-lm配合使用,那麼load/store操作將會轉變為在迴圈前load,在迴圈後store,從而提高執行效率,減少不必要的操作。 

l -fgcse-las:全域性公用子表示式消除pass將消除在store後面的不必要的load操作,這些load與store通常是同一塊儲存單元(全部或區域性) 

l-fdelete-null-pointer-checks:通過對全域性資料流的分析,識別並排出無用的對空指標的檢查。編譯器假設間接引用空指標將停止程式。 如果在間接引用之後檢查指標,它就不可能為空。 

l -fexpensive-optimizations:進行一些從編譯的角度來說代價高昂的優化(這種優化據說對於程式執行未必有很大的好處,甚至有可能降低執行效率,具體不是很清楚) 

l -fregmove:編譯器試圖重新分配move指令或者其他類似運算元等簡單指令的暫存器數目,以便最大化的捆綁暫存器的數目。這種優化尤其對雙運算元指令的機器幫助較大。 

l -fschedule-insns:編譯器嘗試重新排列指令,用以消除由於等待未準備好的資料而產生的延遲。這種優化將對慢浮點運算的機器以及需要load memory的指令的執行有所幫助,因為此時允許其他指令執行,直到load memory的指令完成,或浮點運算的指令再次需要cpu。 l 

-fschedule-insns2:與-fschedule-insns相似。但是當暫存器分配完成後,會請求一個附加的指令計劃pass。這種優化對暫存器較小,並且load memory操作時間大於一個時鐘週期的機器有非常好的效果。 

l -fsched-interblock:這種技術使編譯器能夠跨越指令塊排程指令。 這可以非常靈活地移動指令以便等待期間完成的工作最大化。 

l -fsched-spec-load:允許一些load指令進行一些投機性的動作。(具體不詳)相同功能的還有-fsched-spec-load-dangerous,允許更多的load指令進行投機性操作。這兩個選項在選中-fschedule-insns時預設開啟。 

l -fcaller-saves:通過儲存和恢復call呼叫周圍暫存器的方式,使被call呼叫的value可以被分配給暫存器,這種只會在看上去能產生更好的程式碼的時候才被使用。(如果呼叫多個函式, 這樣能夠節省時間, 因為只進行一次暫存器的儲存和恢復操作, 而不是在每個函式呼叫中都進行。) 

l -fpeephole2:允許計算機進行特定的觀察孔優化(這個不曉得是什麼意思),-fpeephole與-fpeephole2的差別在於不同的編譯器採用不同的方式,由的採用-fpeephole,有的採用-fpeephole2,也有兩種都採用的。 

l -freorder-blocks:在編譯函式的時候重新安排基本的塊,目的在於減少分支的個數,提高程式碼的區域性性。 

l -freorder-functions:在編譯函式的時候重新安排基本的塊,目的在於減少分支的個數,提高程式碼的區域性性。這種優化的實施依賴特定的已存在的資訊:.text.hot用於告知訪問頻率較高的函式,.text.unlikely用於告知基本不被執行的函式。 

l -fstrict-aliasing:這種技術強制實行高階語言的嚴格變數規則。 對於c和c++程式來說, 它確保不在資料型別之間共享變數. 例如, 整數變數不和單精度浮點變數使用相同的記憶體位置。 

l -funit-at-a-time:在程式碼生成前,先分析整個的組合語言程式碼。這將使一些額外的優化得以執行,但是在編譯器間需要消耗大量的記憶體。(有資料介紹說:這使編譯器可以重新安排不消耗大量時間的程式碼以便優化指令快取。) 

l -falign-functions:這個選項用於使函式對準記憶體中特定邊界的開始位置。 大多數處理器按照頁面讀取記憶體,並且確保全部函式程式碼位於單一記憶體頁面內, 就不需要叫化程式碼所需的頁面。 

l -falign-jumps:對齊分支程式碼到2的n次方邊界。在這種情況下,無需執行傀儡指令(dummy operations) 

l -falign-loops:對齊迴圈到2的n次冪邊界。期望可以對迴圈執行多次,用以補償執行dummy operations所花費的時間。 

l -falign-labels:對齊分支到2的n次冪邊界。這種選項容易使程式碼速度變慢,原因是需要插入一些dummy operations當分支抵達usual flow of the code. 

l -fcrossjumping:這是對跨越跳轉的轉換程式碼處理, 以便組合分散在程式各處的相同程式碼。 這樣可以減少程式碼的長度, 但是也許不會對程式效能有直接影響。  

-O3: 比O2更進一步的進行優化。

在包含了O2所有的優化的基礎上,又打開了以下優化選項: 

l -finline-functions:內聯簡單的函式到被呼叫函式中。由編譯器啟發式的決定哪些函式足夠簡單可以做這種內聯優化。預設情況下,編譯器限制內聯的尺寸,3.4.6中限制為600(具體含義不詳,指令條數或程式碼size?)可以通過-finline-limit=n改變這個長度。這種優化技術不為函式建立單獨的組合語言程式碼, 而是把函式程式碼包含在排程程式的程式碼中。 對於多次被呼叫的函式來說, 為每次函式呼叫複製函式程式碼。 雖然這樣對於減少程式碼長度不利, 但是通過最充分的利用指令快取程式碼, 而不是在每次函式呼叫時進行分支操作, 可以提高效能。 

l -fweb:構建用於儲存變數的偽暫存器網路。 偽暫存器包含資料, 就像他們是暫存器一樣, 但是可以使用各種其他優化技術進行優化, 比如cse和loop優化技術。這種優化會使得除錯變得更加的不可能,因為變數不再存放於原本的暫存器中。 

l -frename-registers:在暫存器分配後,通過使用registers left over來避免預定程式碼中的虛假依賴。這會使除錯變得非常困難,因為變數不再存放於原本的暫存器中了。 

l -funswitch-loops:將無變化的條件分支移出迴圈,取而代之的將結果副本放入迴圈中。 

 -Os: 主要是對程式的尺寸進行優化。打開了大部分O2優化中不會增加程式大小的優化選項,並對程式程式碼的大小做更深層的優化。(通常我們不需要這種優化)Os會關閉如下選項: -falign-functions -falign-jumps -falign-loops  -falign-labels   -freorder-blocks   -fprefetch-loop-arrays  

優化介紹小結 O0選項不進行任何優化,在這種情況下,編譯器儘量的縮短編譯消耗(時間,空間),此時,debug會產出和程式預期的結果。當程式執行被斷點打斷,此時程式內的各種宣告是獨立的,我們可以任意的給變數賦值,或者在函式體內把程式計數器指到其他語句,以及從源程式中 精確地獲取你期待的結果. 

O1優化會消耗少多的編譯時間,它主要對程式碼的分支,常量以及表示式等進行優化。 

O2會嘗試更多的暫存器級的優化以及指令級的優化,它會在編譯期間佔用更多的記憶體和編譯時間。 

O3在O2的基礎上進行更多的優化,例如使用偽暫存器網路,普通函式的內聯,以及針對迴圈的更多優化。 

Os主要是對程式碼大小的優化,我們基本不用做更多的關心。 通常各種優化都會打亂程式的結構,讓除錯工作變得無從著手。並且會打亂執行順序,依賴記憶體操作順序的程式需要做相關處理才能確保程式的正確性。  

優化程式碼有可能帶來的問題 

1.除錯問題:正如上面所提到的,任何級別的優化都將帶來程式碼結構的改變。例如:對分支的合併和消除,對公用子表示式的消除,對迴圈內load/store操作的替換和更改等,都將會使目的碼的執行順序變得面目全非,導致除錯資訊嚴重不足。 

2.記憶體操作順序改變所帶來的問題:在O2優化後,編譯器會對影響記憶體操作的執行順序。例如:-fschedule-insns允許資料處理時先完成其他的指令;-fforce-mem有可能導致記憶體與暫存器之間的資料產生類似髒資料的不一致等。對於某些依賴記憶體操作順序而進行的邏輯,需要做嚴格的處理後才能進行優化。例如,採用volatile關鍵字限制變數的操作方式,或者利用barrier迫使cpu嚴格按照指令序執行的。