您的位置:首页 > 其它

gcc 编译过程和编译优化

2017-09-09 15:12 239 查看
编译过程

    

    从源代码(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。猜是什么意思?打开所有种类的警告。很方便吧?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: