gcc 编译过程和编译优化

来源:互联网 发布:钱江晚报微信矩阵 编辑:程序博客网 时间:2024/05/16 18:19
编译过程
    
    从源代码(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。猜是什么意思?打开所有种类的警告。很方便吧?

原创粉丝点击