1. 程式人生 > >gcc 編譯過程和編譯優化

gcc 編譯過程和編譯優化

編譯過程
    
    從原始碼(xxx.cpp)生成可執行檔案(a.out)一共分為四個階段:
    1、預編譯階段:
    此時編譯器會處理原始碼中所有的預編譯指令。預編譯指定非常有特點,全部以“#”開頭。
    想想,以“#”開頭的命令有哪些?
    不同的命令有不同的處理方法,#include命令的處理方法就是赤裸裸的複製貼上。將#include後面的檔案的內容赤裸裸地複製貼上到#include命令所在的位置。#define命令分為帶參巨集和不帶參巨集。#define命令的處理方法,學名叫巨集展開。其實不帶參巨集的處理方法就是赤裸裸的字串替換。之後會產生一篇完全不包含預編譯指令的程式碼。
    使用gcc的-E選項可以檢視預編譯結果:
    g++ -E xxx.cpp
    但是這個命令不會把處理結果儲存在檔案中,而是放在標準輸出中。你可以自己-o定義檔案輸出


    2、彙編階段:
    此時編譯器會將預處理過的程式碼進行彙編。
    使用gcc的-S選項可以檢視彙編結果:
    g++ -S xxx.cpp
    之後會在當前目錄下產生一個xxx.s的檔案,裡面儲存的是彙編程式碼。
    
    3、編譯階段:
    此時編譯器會將彙編程式碼編譯成目標檔案。
    使用gcc的-c選項可以生成目標檔案:
    g++ -c xxx.s
    也可以從原始碼直接生成目標檔案:
    g++ -c xxx.cpp
    gcc會通過副檔名自動判斷處理的是彙編程式碼還是C++程式碼。
    到此,編譯器已經完成它的全部工作。
    
    4、連結階段:
    此時已經沒有編譯器的事情了。連結工作交由連結器來處理。連結器會將多個目標檔案連結成可執行檔案。
    我們可以通過gcc來進行連結,但是實際上,gcc還是呼叫ld命令來完成連結工作的。

    g++ xx1.o xx2.o

編譯期優化選項: -pipe


    上文講到,從原始碼生成最終的可執行檔案需要四個步驟,並且還會產生中間檔案。
    可是我們在對一個原始檔編譯的時候,直接執行g++ xxx.cpp能夠得到可執行檔案a.out,但是並沒有中間檔案啊!中間檔案在哪裡?
    答案是,在/tmp目錄下。想看嗎?跟著我做。
    1、在終端中執行g++ xxx.cpp。
    2、在另外一個終端中執行ls /tmp/cc* 2>/dev/null。
    看見什麼了?什麼也沒有啊!說明你太慢了。
    你需要在第一個命令完成前,執行第二個命令,否則什麼也看不見。你大概只有不到0.1秒的時間。
    寫一個指令碼來看吧。
  #!/bin/bash 
   g++ main.cpp & 
   sleep 0.05 
   ls --color=auto /tmp/cc* 


    在我的電腦上,時間是0.05的時候可以看到如下結果:
    /tmp/cc9CD8ah.o   /tmp/ccj9uXNd.s
    可以看到,有.s彙編檔案,.o目錄檔案。
    所以,實際上gcc將中間檔案放在了/tmp目錄下,並且在編譯完成後會將其刪除。
    可是這樣有一個問題,讀寫檔案都是IO操作,效率上會不會很慢?
    我們需要將上一步的結果交給下一步處理,有沒有什麼比較快的方法?
    如果您瞭解linux的話,會立即想到一個牛X閃閃的東西:管道。
    將上一步編譯的結果通過管道傳遞給下一步,這不需要IO操作,全部在記憶體中完成,效率上會有非常大的提高。
    gcc提供了這個功能,方法是使用-pipe選項。
    g++ -pipe main.cpp
    下面是gcc的man手冊中關於-pipe選項的解釋:
    -pipe 
    Use pipes rather than temporary files for communication between the 
    various stages of compilation.  This fails to work on some systems 
    where the assembler is unable to read from a pipe; but the GNU 
    assembler has no trouble. 

編譯期優化選項: ——O (大寫O)
一段程式碼例子
int createNum(); 
void putNum(int a); 
int sum(int a,int b) 

    return a+b; 

int main() 

    int x=createNum(); 
    int y=createNum(); 
    int z=sum(x,y); 
    putNum( z ); 
    return 0; 
}
    我們來檢視一下它的彙編程式碼:
    g++ -s main.cpp
    得到一個main.s。開啟這個檔案,擷取其中main函式的一小段,加上一些註釋,如下:
call    _Z9createNumv   ;呼叫createNum()函式 
movl    %eax, -4(%rbp)  ;將返回值壓棧 
call    _Z9createNumv   ;再呼叫createNum()函式 
movl    %eax, -8(%rbp)  ;將返回值壓棧 
movl    -8(%rbp), %edx  ;將棧頂資料放在暫存器edx中 
movl    -4(%rbp), %eax  ;同上,放在暫存器eax中 
movl    %edx, %esi      ;將暫存器edx中的資料作為sum()函式的第一個引數 
movl    %eax, %edi      ;將暫存器eax中的資料作為sum()函式的第二個引數 
call    _Z3sumii            ;呼叫sum()函式 
movl    %eax, -12(%rbp) ;將返回值壓棧 
movl    -12(%rbp), %eax ;將棧頂資料放在暫存器eax中 
movl    %eax, %edi      ;將暫存器eax中的資料作為putNum()函式的第一個引數 
call    _Z6putNumi      ;呼叫putNum()函式
  大家覺得,是不是很麻煩?
    每次呼叫一個函式之後,先壓棧,然後又轉到暫存器中,這很浪費時間。
    gcc會這麼笨嗎?當然不會。gcc的-O選項(注意,是大寫。回想一下,小寫-o選項是幹什麼的?前面講過)就是用來處理編譯期優化的。我們重新產生一下彙編程式碼,但是使用-O選項。
    g++ -O -s main.cpp -o main.O1.s
    現在開啟main.O1.s檔案,看,裡面函式的返回值沒有經過入棧和出棧的過程,直接傳入下一個函式的引數。這樣減少了六條彙編程式碼。
    可是細想想,sum()函式有點多餘。它實際上只是做了一個加法,但是我們仍然需要呼叫這個函式來完成它的功能。我們知道,呼叫一個函式就需要幾條到十幾條彙編程式碼,這是很浪費時間的。gcc會這麼笨嗎?當然不會。-O選項還可以加數字,表示優化的級別。沒有數字預設是1,最大可以加到3。優化級別越高,產生的程式碼的執行效率就越高。我們用級別2試一下:
    g++ -O2 -s main.cpp -o main.O2.s
    現在開啟main.O2.s,大家可以看到,呼叫sum()函式的程式碼都不見了,取而代之的是一條加法指令來完成兩個整數的相加。
    -O3的效果我就不試了。而且,如果不加-O選項,優化級別就是0。
    
    既然-O後面的的數字越大,產生的程式碼越優化,那麼為什麼不直接用-O3?原因是,優化的級別越高,雖然最後生成的程式碼的執行效率就會越高,但是編譯的過程花費的時間就會越長。如果你曾經編譯過大的軟體(比如下載KDE原始碼,然後編譯安裝),你就會知道,相比於JAVA等語言,C++的編譯效率是非常低的。同樣都是100萬行程式碼,C++編譯它需要四個小時,JAVA可能只需要十分鐘。所以,在執行效率和編譯時間之間,需要做出一個權衡。gcc沒有擅自做這個決定,而是把決定的權力留給了使用者。
    在linux的世界裡,有這樣一個觀點:讓軟體儘可能少的代替使用者做出決定,讓使用者能夠儘可能多的做自己想要的效果。所以linux的很多軟體都有很多選項

   下面的內容有些無聊,只是把O選項相關的文件翻譯出來。想了解的可以瞭解下,想深入瞭解的可以去看gcc的man手冊。
    括號裡面的是我自己的想法,剩下的是gcc的man手冊中關於O選項的翻譯。
    
    -O
    -O1 優化。優化編譯將多花費一些時間,還會在編譯大函式的時候消耗更多的記憶體。
        加上-O選項以後,編譯器試圖減少生成可執行檔案的大小和執行時間。相比於不加優化將花費大量的編譯時間。
        
        -O選項啟用以下優化器:
        -fauto-inc-dec -fcprop-registers -fdce -fdefer-pop -fdelayed-branch
        -fdse -fguess-branch-probability -fif-conversion2 -fif-conversion
        -fipa-pure-const -fipa-reference -fmerge-constants -fshrink-wrap
        -fsplit-wide-types -ftree-builtin-call-dce -ftree-ccp -ftree-ch
        -ftree-copyrename -ftree-dce -ftree-dominator-opts -ftree-dse
        -ftree-forwprop -ftree-fre -ftree-phiprop -ftree-sra -ftree-pta
        -ftree-ter -funit-at-a-time
        
        在那些不會影響除錯的裝置上,-O選項也會啟動-fomit-frame-pointer優化器。
        
    -O2 更多的優化。GCC將在不需要用空間換取時間的條件下,啟用幾乎所有支援的優化器。與-O選項比較,這個選項雖然增加了編譯的時間,但生成的程式碼更加高效了。
        
        -O2選項除了啟用-O選項的所有優化器外,還將啟用以下優化器:
        -fthread-jumps
       -falign-functions  -falign-jumps -falign-loops  -falign-labels
       -fcaller-saves -fcrossjumping -fcse-follow-jumps  -fcse-skip-blocks
       -fdelete-null-pointer-checks -fexpensive-optimizations -fgcse
       -fgcse-lm -finline-small-functions -findirect-inlining -fipa-sra
       -foptimize-sibling-calls -fpeephole2 -fregmove -freorder-blocks
       -freorder-functions -frerun-cse-after-loop -fsched-interblock
       -fsched-spec -fschedule-insns  -fschedule-insns2 -fstrict-aliasing
       -fstrict-overflow -ftree-if-to-switch-conversion
       -ftree-switch-conversion -ftree-pre -ftree-vrp
    
    -O3 最多的優化。-O3選項啟用-O2的全部優化器,還將啟用以下優化器:
        -finline-functions, -funswitch-loops,
       -fpredictive-commoning, -fgcse-after-reload, -ftree-vectorize and
       -fipa-cp-clone options.


    -O0 減少編譯時間並讓除錯程式得到期望的結果。這個是預設值。
    
    -Os 空間優化。-Os啟用-O2選項的所有不會增加生成可執行檔案大小的優化器外,還會為減少生成可執行檔案的大小做更多的優化。
        -Os禁用以下優化器:-falign-functions
       -falign-jumps  -falign-loops -falign-labels  -freorder-blocks
       -freorder-blocks-and-partition -fprefetch-loop-arrays
       -ftree-vect-loop-version
       
       如果您同時啟用多個-O選項,無論有沒有級別數字,只有最後一個選項有效。

編譯期優化選項: -W
 
    優秀的程式設計師不應該忽略任何的warning。
    優秀的程式設計師寫的程式碼不但沒有error,還沒有warning。
     看一段程式碼
    int fun(){ 
     } 
   int main(){ 
    fun(); 
    } 
  很簡單,對吧?
    有錯誤嗎?事實上是沒有的。
    編譯一下:g++ return-type.cpp。也沒有任何問題。
    可是事實上,fun函式沒有return語句,那麼它可能會返回一個隨機的值,這種忽略可能會造成嚴重的錯誤。
    我們希望,gcc在遇見這類問題的時候,能夠給我們一個提示。
    還好,gcc提供了一個-W選項。
    我們使用這樣的命令來編譯:
    g++ -Wreturn-type return-type.cpp
    它仍然能夠正常編譯,生成可執行檔案,但是,它會輸出一句warning:
return-type.cpp: In function ‘int fun()’:
return-type.cpp:3:1: warning: no return statement in function returning non-void
    不錯吧?
    解釋一下,-W是開啟警告輸出,後面接的是警告的種類。gcc將警告分為好多種(將近一百種)。return-type只是檢查返回值型別。
   再看一段程式碼:
    int fun(){ 
    int a; 
    return a; 
     } 
    int main(){ 
    fun(); 
     } 
   按照正常方式編譯:g++ uninitialized.cpp。沒有任何問題。
    我們開啟uninitialized種類的警告,這樣編譯:
    g++ -Wuninitialized uninitialized.cpp
    它輸出的warning是這樣的:
uninitialized.cpp: In function ‘int fun()’:
uninitialized.cpp:4:12: warning: ‘a’ is used uninitialized in this function
    
    但是,種類那麼多,一個一個加會不會很麻煩?
    哈哈!gcc的-W選項有個種類叫all。猜是什麼意思?開啟所有種類的警告。很方便吧?