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。猜是什么意思?打开所有种类的警告。很方便吧?
- gcc 编译过程和编译优化
- Linux下GCC的DEBUG和优化,以及编译过程
- Linux GCC常用命令和编译过程
- GCC编译优化指南
- gcc 编译优化
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化指南
- GCC编译优化选项
- GCC编译优化指南
- GCC编译优化指南
- Java开发者必读的10篇精选优秀技术文章
- 单源最短路问题 Codevs 1557 热浪(含讲解)
- html1、2两天补充
- mysql索引
- Android View事件传播机制
- gcc 编译过程和编译优化
- ngrx实例
- 电路第一章知识点总结(上)
- 协方差和想关系数理解
- txt导入mysql
- linux命令--查找与统计(grep、awk、sort、uniq、wc)
- __builtin_expect 解惑
- 学生中遇到的问题(一)
- 微信公众号开发——1、搭建服务器+与用户交流