首页 > Computer > gcc详解
2011
09-09

gcc详解

在为Linux开发应用程序时,绝大多数情况下使用的都是C语言,因此几乎每一位Linux程序员面临的首要问题都是如何灵活运用C编译器.目前Linux下最常用的C语言编译器是GCC(GNU Compiler Collection),它是GNU项目中符合ANSI C标准的编译系统,能够编译用C、C++和Object C等语言编写的程序.GCC不仅功能非常强大,结构也异常灵活.最值得称道的一点就是它可以通过不同的前端模块来支持各种语言,如Java、Fortran、Pascal、Modula-3和Ada等. 开放、自由和灵活是Linux的魅力所在,而这一点在GCC上的体现就是程序员通过它能够更好地控制整个编译过程.在使用GCC编译程序时,编译过程可以被细分为四个阶段:

◆ 预处理(Pre-Processing)

◆ 编译(Compiling)

◆ 汇编(Assembling)

◆ 链接(Linking)

Linux程序员可以根据自己的需要让GCC在编译的任何阶段结束,以便检查或使用编译器在该阶段的输出信息,或者对最后生成的二进制文件进行控制,以便通过加入不同数量和种类的调试代码来为今后的调试做好准备.和其它常用的编译器一样,GCC也提供了灵活而强大的代码优化功能,利用它可以生成执行效率更高的代码.

GCC提供了30多条警告信息和三个警告级别,使用它们有助于增强程序的稳定性和可移植性.此外,GCC还对标准的C和C++语言进行了大量的扩展,提高程序的执行效率,有助于编译器进行代码优化,能够减轻编程的工作量.

GCC起步

在学习使用GCC之前,下面的这个例子能够帮助用户迅速理解GCC的工作原理,并将其立即运用到实际的项目开发中去.

首先用熟悉的编辑器输入清单1所示的代码:

清单1:hello.c

#include <stdio.h>
int main(void)
{
printf (“Hello world, Linux programming!/n”);
return 0;
}

然后执行下面的命令编译和运行这段程序:

# gcc hello.c -o hello
# ./hello
Hello world, Linux programming!

程序员的角度看,只需简单地执行一条GCC命令就可以了,但从编译器的角度来看,却需要完成一系列非常繁杂的工作.首先,GCC需要调用预处理程序cpp,由它负责展开在源文件中定义的宏,并向其中插入“#include”语句所包含的

内容;接着,GCC会调用ccl和as将处理后的源代码编译成目标代码;最后,GCC会调用链接程序ld,把生成的目标代码链接成一个可执行程序.

为了更好地理解GCC的工作过程,可以把上述编译过程分成几个步骤单独进行,并观察每步的运行结果.第一步是进行预编译,使用-E参数可以让GCC在预处理结束后停止编译过程:

# gcc -E hello.c -o hello.i

此时若查看hello.cpp文件中的内容,会发现stdio.h的内容确实都插到文件里去了,而其它应当被预处理的宏定义也

都做了相应的处理.下一步是将hello.i编译为目标代码,这可以通过使用-c参数来完成:

# gcc -c hello.i -o hello.o

GCC默认将.i文件看成是预处理后的C语言源代码,因此上述命令将自动跳过预处理步骤而开始执行编译过程,也可以使用-x参数让GCC从指定的步骤开始编译.最后一步是将生成的目标文件链接成可执行文件:

# gcc hello.o -o hello

在采用模块化的设计思想进行软件开发时,通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用GCC能够很好地管理这些编译单元.假设有一个由foo1.c和foo2.c两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序foo,可以使用下面这条命令:

# gcc foo1.c foo2.c -o foo

如果同时处理的文件不止一个,GCC仍然会按照预处理、编译和链接的过程依次进行.如果深究起来,上面这条命令大致相当于依次执行如下三条命令:

# gcc -c foo1.c -o foo1.o
# gcc -c foo2.c -o foo2.o
# gcc foo1.o foo2.o -o foo

在编译一个包含许多源文件的工程时,若只用一条GCC命令来完成编译是非常浪费时间的.假设项目中有100个源文件需要编译,并且每个源文件中都包含10000行代码,如果像上面那样仅用一条GCC命令来完成编译工作,那么GCC需要将每个源文件都重新编译一遍,然后再全部连接起来.很显然,这样浪费的时间相当多,尤其是当用户只是修改了其中某一个文件的时候,完全没有必要将每个文件都重新编译一遍,因为很多已经生成的目标文件是不会改变的.要解决这个问题,关键是要灵活运用GCC,同时还要借助像Make这样的工具.

警告提示功能

GCC包含完整的出错检查和警告提示功能,它们可以帮助Linux程序员写出更加专业和优美的代码.先来读读清单2所示的程序,这段代码写得很糟糕,仔细检查一下不难挑出很多毛病:

◆main函数的返回值被声明为void,但实际上应该是int;

◆使用了GNU语法扩展,即使用long long来声明64位整数,不符合ANSI/ISO C语言标准;

◆main函数在终止前没有调用return语句.

清单2:illcode.c

#include <stdio.h>
void main(void)
{
long long int var = 1;
printf(“It is not standard C code!/n”);
}

下面来看看GCC是如何帮助程序员来发现这些错误的.当GCC在编译不符合ANSI/ISO C语言标准的源代码时,如果加上了-pedantic选项,那么使用了扩展语法的地方将产生相应的警告信息:

# gcc -pedantic illcode.c -o illcode
illcode.c: In function `main’:
illcode.c:9: ISO C89 does not support `long long’
illcode.c:8: return type of `main’ is not `int’

需要注意的是,-pedantic编译选项并不能保证被编译程序与ANSI/ISO C标准的完全兼容,它仅仅只能用来帮助Linux程序员离这个目标越来越近.或者换句话说,-pedantic选项能够帮助程序员发现一些不符合ANSI/ISO C标准的代码,但不是全部,事实上只有ANSI/ISO C语言标准中要求进行编译器诊断的那些情况,才有可能被GCC发现并提出警告.

除了-pedantic之外,GCC还有一些其它编译选项也能够产生有用的警告信息.这些选项大多以-W开头,其中最有价值的当数-Wall了,使用它能够使GCC产生尽可能多的警告信息:

# gcc -Wall illcode.c -o illcode
illcode.c:8: warning: return type of `main’ is not `int’
illcode.c: In function `main’:
illcode.c:9: warning: unused variable `var’

GCC给出的警告信息虽然从严格意义上说不能算作是错误,但却很可能成为错误的栖身之所.一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持简洁、优美和健壮的特性.

在处理警告方面,另一个常用的编译选项是-Werror,它要求GCC将所有的警告当成错误进行处理,这在使用自动编译工具(如Make等)时非常有用.如果编译时带上-Werror选项,那么GCC会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改.只有当相应的警告信息消除时,才可能将编译过程继续朝前推进.执行情况如下:

# gcc -Wall -Werror illcode.c -o illcode
cc1: warnings being treated as errors
illcode.c:8: warning: return type of `main’ is not `int’
illcode.c: In function `main’:
illcode.c:9: warning: unused variable `var’

对Linux程序员来讲,GCC给出的警告信息是很有价值的,它们不仅可以帮助程序员写出更加健壮的程序,而且还是跟踪和调试程序的有力工具.建议在用GCC编译源代码时始终带上-Wall选项,并把它逐渐培养成为一种习惯,这对找出常见的隐式编程错误很有帮助.

库依赖

在Linux下开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助一个或多个函数库的支持才能够完成相应的功能.从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(.so或者.a)的集合.虽然Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下,但并不是所有的情况都是这样.正因如此,GCC在编译时必须有自己的办法来查找所需要的头文件和库文件.

GCC采用搜索目录的办法来查找所需要的文件,-I选项可以向GCC的头文件搜索路径中添加新的目录.例如,如果

在/home/xiaowp/include/目录下有编译时所需要的头文件,为了让GCC能够顺利地找到它们,就可以使用-I选项:

# gcc foo.c -I /home/xiaowp/include -o foo

同样,如果使用了不在标准位置的库文件,那么可以通过-L选项向GCC的库文件搜索路径中添加新的目录.例如,如果在/home/xiaowp/lib/目录下有链接时所需要的库文件libfoo.so,为了让GCC能够顺利地找到它,可以使用下面的命令:

# gcc foo.c -L /home/xiaowp/lib -lfoo -o foo

值得好好解释一下的是-l选项,它指示GCC去连接库文件libfoo.so.Linux下的库文件在命名时有一个约定,那就是应

该以lib三个字母开头,由于所有的库文件都遵循了同样的规范,因此在用-l选项指定链接的库文件名时可以省去lib

三个字母,也就是说GCC在对-lfoo进行处理时,会自动去链接名为libfoo.so的文件.

Linux下的库文件分为两大类分别是动态链接库(通常以.so结尾)和静态链接库(通常以.a结尾),两者的差别仅在程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的.默认情况下,GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库.例如,如果在/home/xiaowp/lib/目录下有链接时所需要的库文件libfoo.so和libfoo.a,为了让GCC在链接时只用

到静态链接库,可以使用下面的命令:

# gcc foo.c -L /home/xiaowp/lib -static -lfoo -o foo
代码优化

代码优化指的是编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能.GCC提供的代码优化功能非常强大,它通过编译选项-On来控制优化代码的生成,其中n是一个代表优化级别的整数.对于不同版本的GCC来讲,n的取值范围及其对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3.

编译时使用选项-O可以告诉GCC同时减小代码的长度和执行时间,其效果等价于-O1.在这一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化.选项-O2告诉GCC除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等.选项-O3则除了完成所有-O2级别的优化之外,还包括循环展开和其它一些与处理器特性相关的优化工作.通常来说,数字越大优化的等级越高,同时也就意味着程序的运行速度越快.许多Linux程序员都喜欢使用-O2选项,因为它在优化长度、编译时间和代码大小之间,取得了一个比较理想的平衡点.

下面通过具体实例来感受一下GCC的代码优化功能,所用程序如清单3所示.

清单3:optimize.c

#include <stdio.h>
int main(void)
{
double counter;
double result;
double temp;
for (counter = 0;
counter < 2000.0 * 2000.0 * 2000.0 / 20.0 + 2020;
counter += (5 – 1) / 4) {
temp = counter / 1979;
result = counter;
}
printf(“Result is %lf/n”, result);
return 0;
}

首先不加任何优化选项进行编译:

# gcc -Wall optimize.c -o optimize

借助Linux提供的time命令,可以大致统计出该程序在运行时所需要的时间:

# time ./optimize
Result is 400002019.000000
real 0m14.942s
user 0m14.940s
sys 0m0.000s

接下去使用优化选项来对代码进行优化处理:

# gcc -Wall -O optimize.c -o optimize

在同样的条件下再次测试一下运行时间:

# time ./optimize
Result is 400002019.000000
real 0m3.256s
user 0m3.240s
sys 0m0.000s

对比两次执行的输出结果不难看出,程序的性能的确得到了很大幅度的改善,由原来的14秒缩短到了3秒.这个例子是专门针对GCC的优化功能而设计的,因此优化前后程序的执行速度发生了很大的改变.尽管GCC的代码优化功能非常强大,但作为一名优秀的Linux程序员,首先还是要力求能够手工编写出高质量的代码.如果编写的代码简短,并且逻辑性强,编译器就不会做更多的工作,甚至根本用不着优化.

优化虽然能够给程序带来更好的执行性能,但在如下一些场合中应该避免优化代码:

程序开发的时候 优化等级越高,消耗在编译上的时间就越长,因此在开发的时候最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的代码进行优化.

◆ 资源受限的时候 一些优化选项会增加可执行代码的体积,如果程序在运行时能够申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,因为由这带来的负面影响可能会产生非常严重的后果.

◆ 跟踪调试的时候 在对代码进行优化的时候,某些代码可能会被删除或改写,或者为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难.

调试

一个功能强大的调试器不仅为程序员提供了跟踪程序执行的手段,而且还可以帮助程序员找到解决问题的方法.对于Linux程序员来讲,GDB(GNU Debugger)通过与GCC的配合使用,为基于Linux的软件开发提供了一个完善的调试环境.

默认情况下,GCC在编译时不会将调试符号插入到生成的二进制代码中,因为这样会增加可执行文件的大小.如果需要在编译时生成调试符号信息,可以使用GCC的-g或者-ggdb选项.GCC在产生调试符号时,同样采用了分级的思路,开发人员可以通过在-g选项后附加数字1、2或3来指定在代码中加入调试信息的多少.默认的级别是2(-g2),此时产生的调试信息包括扩展的符号表、行号、局部或外部变量信息.级别3(-g3)包含级别2中的所有调试信息,以及源代码中定义的宏.级别1(-g1)不包含局部变量和与行号有关的调试信息,因此只能够用于回溯跟踪和堆栈转储之用.回溯跟踪指的是监视程序在运行过程中的函数调用历史,堆栈转储则是一种以原始的十六进制格式保存程序执行环境的方法,两者都是经常用到的调试手段.

GCC产生的调试符号具有普遍的适应性,可以被许多调试器加以利用,但如果使用的是GDB,那么还可以通过-ggdb选项在生成的二进制代码中包含GDB专用的调试信息.这种做法的优点是可以方便GDB的调试工作,但缺点是可能导致其它调试器(如DBX)无法进行正常的调试.选项-ggdb能够接受的调试级别和-g是完全一样的,它们对输出的调试符号有着相同的影响.

需要注意的是,使用任何一个调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时的开销,因此调试选项通常仅在软件的开发和调试阶段使用.调试选项对生成代码大小的影响从下面的对比过程中可以看出来:

# gcc optimize.c -o optimize
# ls optimize -l
-rwxrwxr-x 1 xiaowp xiaowp 11649 Nov 20 08:53 optimize (未加调试选项)
# gcc -g optimize.c -o optimize
# ls optimize -l
-rwxrwxr-x 1 xiaowp xiaowp 15889 Nov 20 08:54 optimize (加入调试选项)

虽然调试选项会增加文件的大小,但事实上Linux中的许多软件在测试版本甚至最终发行版本中仍然使用了调试选项来进行编译,这样做的目的是鼓励用户在发现问题时自己动手解决,是Linux的一个显著特色.

为调试编译代码(Compiling Code for Debugging)
为了使 gdb 正常工作, 你必须使你的程序在编译时包含调试信息. 调试信息包含你程序里的每个变量的类型和在可执行文件里的地址映射以及源代码的行号. gdb 利用这些信息使源代码和机器码相关联.
◆在编译时用 -g 选项打开调试选项.

gdb 基本命令
gdb 支持很多的命令使你能实现不同的功能. 这些命令从简单的文件装入到允许你检查所调用的堆栈内容的复

杂命令, 表27.1列出了你在用 gdb 调试时会用到的一些命令. 想了解 gdb 的详细使用请参考 gdb 的指南页.
表 27.1. 基本 gdb 命令.
命 令 描 述
file 装入想要调试的可执行文件.
kill 终止正在调试的程序.
list 列出产生执行文件的源代码的一部分.
next 执行一行源代码但不进入函数内部.
step 执行一行源代码而且进入函数内部.
run 执行当前被调试的程序
quit 终止 gdb
watch 使你能监视一个变量的值而不管它何时被改变.
break 在代码里设置断点, 这将使程序执行到这里时被挂起.
make 使你能不退出 gdb 就可以重新产生可执行文件.
shell 使你能不离开 gdb 就执行 UNIX shell 命令.

gdb 支持很多与 UNIX shell 程序一样的命令编辑特征. 你能象在 bash 或 tcsh里那样按 Tab 键让gdb 帮你补齐

一个唯一的命令, 如果不唯一的话 gdb 会列出所有匹配的命令. 你也能用光标键上下翻动历史命令.

下面还是通过一个具体的实例说明如何利用调试符号来分析错误,所用程序见清单4所示.

清单4:crash.c

#include <stdio.h>
int main(void)
{
int input =0;
printf(“Input an integer:”);
scanf(“%d”, input);
printf(“The integer you input is %d/n”, input);
return 0;
}

编译并运行上述代码,会产生一个严重的段错误(Segmentation fault)如下:

# gcc -g crash.c -o crash
# ./crash
Input an integer:10
Segmentation fault

为了更快速地发现错误所在,可以使用GDB进行跟踪调试,方法如下:

# gdb crash
GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)
……
(gdb)

当GDB提示符出现的时候,表明GDB已经做好准备进行调试了,现在可以通过run命令让程序开始在GDB的监控下运行:

(gdb) run
Starting program: /home/xiaowp/thesis/gcc/code/crash
Input an integer:10

Program received signal SIGSEGV, Segmentation fault.
0x4008576b in _IO_vfscanf_internal () from /lib/libc.so.6

仔细分析一下GDB给出的输出结果不难看出,程序是由于段错误而导致异常中止的,说明内存操作出了问题,具体发生问题的地方是在调用_IO_vfscanf_internal ( )的时候.为了得到更加有价值的信息,可以使用GDB提供的回溯跟踪命令backtrace,执行结果如下:

(gdb) backtrace
#0 0x4008576b in _IO_vfscanf_internal () from /lib/libc.so.6
#1 0xbffff0c0 in ?? ()
#2 0x4008e0ba in scanf () from /lib/libc.so.6
#3 0x08048393 in main () at crash.c:11
#4 0x40042917 in __libc_start_main () from /lib/libc.so.6

跳过输出结果中的前面三行,从输出结果的第四行中不难看出,GDB已经将错误定位到crash.c中的第11行了.现在仔细检查一下:

(gdb) frame 3
#3 0x08048393 in main () at crash.c:11
11 scanf(“%d”, input);

使用GDB提供的frame命令可以定位到发生错误的代码段,该命令后面跟着的数值可以在backtrace命令输出结果中的行首找到.现在已经发现错误所在了,应该将

scanf(“%d”, input);
改为
scanf(“%d”, &input);

完成后就可以退出GDB了,命令如下:

(gdb) quit

GDB的功能远远不止如此,它还可以单步跟踪程序、检查内存变量和设置断点等.

调试时可能会需要用到编译器产生的中间结果,这时可以使用-save-temps选项,让GCC将预处理代码、汇编代码和目标代码都作为文件保存起来.如果想检查生成的代码是否能够通过手工调整的办法来提高执行性能,在编译过程中生成的中间文件将会很有帮助,具体情况如下:

# gcc -save-temps foo.c -o foo
# ls foo*
foo foo.c foo.i foo.s

GCC支持的其它调试选项还包括-p和-pg,它们会将剖析(Profiling)信息加入到最终生成的二进制代码中.剖析信息对于找出程序的性能瓶颈很有帮助,是协助Linux程序员开发出高性能程序的有力工具.在编译时加入-p选项会在生成的代码中加入通用剖析工具(Prof)能够识别的统计信息,而-pg选项则生成只有GNU剖析工具(Gprof)才能识别的统计信息.

最后提醒一点,虽然GCC允许在优化的同时加入调试符号信息,但优化后的代码对于调试本身而言将是一个很大的挑战.代码在经过优化之后,在源程序中声明和使用的变量很可能不再使用,控制流也可能会突然跳转到意外的地方,循环语句有可能因为循环展开而变得到处都有,所有这些对调试来讲都将是一场噩梦.建议在调试的时候最好不使用任何优化选项,只有当程序在最终发行的时候才考虑对其进行优化.

上次的培训园地中介绍了GCC的编译过程、警告提示功能、库依赖、代码优化和程序调试六个方面的内容.这期是最后的一部分内容.

加速

在将源代码变成可执行文件的过程中,需要经过许多中间步骤,包含预处理、编译、汇编和连接.这些过程实际上是由不同的程序负责完成的.大多数情况下GCC可以为Linux程序员完成所有的后台工作,自动调用相应程序进行处理.

这样做有一个很明显的缺点,就是GCC在处理每一个源文件时,最终都需要生成好几个临时文件才能完成相应的工作,从而无形中导致处理速度变慢.例如,GCC在处理一个源文件时,可能需要一个临时文件来保存预处理的输出、一个临时文件来保存编译器的输出、一个临时文件来保存汇编器的输出,而读写这些临时文件显然需要耗费一定的时间.当软件项目变得非常庞大的时候,花费在这上面的代价可能会变得很沉重.

解决的办法是,使用Linux提供的一种更加高效的通信方式—管道.它可以用来同时连接两个程序,其中一个程序的输出将被直接作为另一个程序的输入,这样就可以避免使用临时文件,但编译时却需要消耗更多的内存.

在编译过程中使用管道是由GCC的-pipe选项决定的.下面的这条命令就是借助GCC的管道功能来提高编译速度的:

# gcc -pipe foo.c -o foo

在编译小型工程时使用管道,编译时间上的差异可能还不是很明显,但在源代码非常多的大型工程中,差异将变得非常明显.

文件扩展名

在使用GCC的过程中,用户对一些常用的扩展名一定要熟悉,并知道其含义.为了方便大家学习使用GCC,在此将这些扩展名罗列如下:

.c C原始程序

.C C++原始程序

.cc C++原始程序

.cxx C++原始程序

.m Objective-C原始程序

.i 已经过预处理的C原始程序

.ii 已经过预处理之C++原始程序

.s 组合语言原始程序

.S 组合语言原始程序

.h 预处理文件(标头文件);

.o 目标文件

.a 存档文件.

GCC常用选项

GCC作为Linux下C/C++重要的编译环境,功能强大,编译选项繁多.为了方便大家日后编译方便,在此将常用的选项及说明罗列出来如下:

-c 通知GCC取消链接步骤,即编译源码并在最后生成目标文件

-Dmacro 定义指定的宏,使它能够通过源码中的#ifdef进行检验;

-E 不经过编译预处理程序的输出而输送至标准输出;

-g3 获得有关调试程序的详细信息,它不能与-o选项联合使用;

-Idirectory 在包含文件搜索路径的起点处添加指定目录;

-llibrary 提示链接程序在创建最终可执行文件时包含指定的库;

-O、-O2、-O3 将优化状态打开,该选项不能与-g选项联合使用;

-S 要求编译程序生成来自源代码的汇编程序输出;

-v 启动所有警报;

-Wall 在发生警报时取消编译操作,即将警报看作是错误;

-Werror 在发生警报时取消编译操作,即把报警当作是错误;

-w 禁止所有的报警.

小结

GCC是在Linux下开发程序时必须掌握的工具之一.本文对GCC做了一个简要的介绍,主要讲述了如何使用GCC编译程序、产生警告信息、调试程序和加快GCC的编译速度.对所有希望早日跨入Linux开发者行列的人来说,GCC就是成为一名

GCC编译优化指南

作者:金步国


版权声明

本文作者是一位自由软件爱好者,所以本文虽然不是软件,但是本着 GPL 的精神发布。任何人都可以自由使用、转载、复制和再分发,但必须保留作者署名,亦不得对声明中的任何条款作任何形式的修改,也不得附加任何其它条件。您可以自由链接、下载、传播此文档,但前提是必须保证全文完整转载,包括完整的版权信息和作译者声明。

其他作品

本文作者十分愿意与他人共享劳动成果,如果你对我的其他翻译作品或者技术文章有兴趣,可以在如下位置查看现有作品的列表:

BUG报告,切磋与探讨

由于作者水平有限,因此不能保证作品内容准确无误,请在阅读中自行鉴别。如果你发现了作品中的错误,请您来信指出,哪怕是错别字也好,任何提高作品质量的建议我都将虚心接纳。如果你愿意就作品中的相关内容与我进行进一步切磋与探讨,也欢迎你与我联系。联系方式:MSN: csfrank122@hotmail.com


前言

网上关于编译优化的文章很多,但大多零零散散,不成体系,本文试图给出一个完整和清晰的优化思路,同时提供在实践中如何进行优化的详尽参考。但是,在介绍所有优化知识之前首先引用LFS-Book中的一句忠告:“使用编译器优化得到的小幅度性能提升,与它带来的风险相比微不足道”。你还要进行优化吗?

%@&#=^%~*# …
OK, crazy guy! Let’s Go!!

在继续之前,作者还是奉劝各位:如果追求极致的优化,那么它将是一件既耗时又麻烦的事情,你会陷入无止尽的测试、测试、再测试……另外 Gentoo wiki 上有这么一句话:”GCC has well over a hundred individual optimization flags and it would be insane to try and describe them all.”所以本文不会涉及全部GCC优化选项。最后作者还是再罗唆一句:优化应当适可而止为好,将精力留出来做一些其它事情会更有意义!

先决条件

本文的主要读者是 LFS/Gentoo 的玩家,基本上比较 crazy 的玩家都接触过,如果你之前从未使用过 LFS/Gentoo ,请先按照《Linux From Scratch 6.2 中文版》做一遍 LFS ,然后再来阅读此文将会更有意义。另外,本文是建立在《深入理解软件包的配置、编译与安装》一文基础之上的,在开始阅读本文之前,请先阅读它。

基本原理

我们首先从三个方面来看与优化相关的内容:

  1. 从运行时的依赖关系来看,对性能有较大影响的组件有 kernel 和 glibc ,虽然这严格说来这不属于本文的话题,但是经过精心选择、精心配置、精心编译的内核与C库将对提高系统的运行速度起着基础性的作用。
  2. 从被编译的软件包来看,每个软件包的 configure 脚本都提供了许多配置选项,其中有许多选项是与性能息息相关的。比如,对于 Apache-2.2.6 而言,你可以使用 –enable-MODULE=static 将模块静态编译进核心,使用 –disable-MODULE 禁用不需要的模块,使用 –with-mpm=MPM 选择一个高效的多路处理模块,在不需要IPv6的情况下使用 –disable-ipv6 禁用IPv6支持,在不使用线程化的MPM时使用 –disable-threads 禁用线程支持,等等……这部分内容显然不可能在本文中进行完整的讲述,本文只能讲述与优化相关的通用选项。针对特定的软件包,请在编译前使用 configure –help 查看所有选项,并精心选择。
  3. 从编译过程自身来看,将源代码编译为二进制文件是在 Makefile 文件的指导下,由 make 程序调用一条条编译命令完成的。而将源代码编译为二进制文件又需要经过以下四个步骤:预处理(cpp) → 编译(gcc或g++) → 汇编(as) → 连接(ld) ;括号中表示每个阶段所使用的程序,它们分别属于 GCC 和 Binutils 软件包。显然的,优化应当从编译工具自身的选择以及控制编译工具的行为入手。

大体上编译优化就这”三板斧”(其实是”三脚猫”)了,本文接下来的内容将讨论这只猫的后两只脚。

编译工具的选择

对于编译工具自身的选择,在假定使用 Binutils 和 GCC 以及 Make 的前提下,没什么好说的,基本上新版本都能带来性能提升,同时比老版本对新硬件的支持更好,所以应当尽量选用新版本。不过追新也可能带来系统的不稳定,这就要针对实际情况进行权衡了。本文以 Binutils-2.18 和 GCC-4.2.2/GCC-4.3.0 以及 Make-3.81 为例进行说明。

configure 选项

这里我们只讲解通用的”体系结构选项”,由于”特性选项”在每个软件包之间千差万别,所以不可能在此处进行讲解。

这部分内容很简单,并且其含义也是不言而喻的,下面只列出常用的值:

  • i586-pc-linux-gnu
  • i686-pc-linux-gnu
  • x86_64-pc-linux-gnu
  • powerpc-unknown-linux-gnu
  • powerpc64-unknown-linux-gnu

如果你实在不知道应当使用哪一个,那么就干脆不使用这几个选项,让 config.guess 脚本自己去猜吧,反正也挺准的。

编译选项

让我们先看看 Makefile 规则中的编译命令通常是怎么写的。

大多数软件包遵守如下约定俗成的规范:

#1,首先从源代码生成目标文件(预处理,编译,汇编),"-c"选项表示不执行链接步骤。
$(CC) $(CPPFLAGS) $(CFLAGS) example.c   -c   -o example.o
#2,然后将目标文件连接为最终的结果(连接),"-o"选项用于指定输出文件的名字。
$(CC) $(LDFLAGS) example.o   -o example

#有一些软件包一次完成四个步骤:
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) example.c   -o example

当然也有少数软件包不遵守这些约定俗成的规范,比如:

#1,有些在命令行中漏掉应有的Makefile变量(注意:有些遗漏是故意的)
$(CC) $(CFLAGS) example.c    -c   -o example.o
$(CC) $(CPPFLAGS) example.c  -c   -o example.o
$(CC) example.o   -o example
$(CC) example.c   -o example
#2,有些在命令行中增加了不必要的Makefile变量
$(CC) $(CFLAGS) $(LDFLAGS) example.o   -o example
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) example.c   -c   -o example.o

当然还有极个别软件包完全是”胡来”:乱用变量(增加不必要的又漏掉了应有的)者有之,不用$(CC)者有之,不一而足…..

尽管将源代码编译为二进制文件的四个步骤由不同的程序(cpp,gcc/g++,as,ld)完成,但是事实上 cpp, as, ld 都是由 gcc/g++ 进行间接调用的。换句话说,控制了 gcc/g++ 就等于控制了所有四个步骤。从 Makefile 规则中的编译命令可以看出,编译工具的行为全靠 CC/CXX CPPFLAGS CFLAGS/CXXFLAGS LDFLAGS 这几个变量在控制。当然理论上控制编译工具行为的还应当有 AS ASFLAGS ARFLAGS 等变量,但是实践中基本上没有软件包使用它们。

那么我们如何控制这些变量呢?一种简易的做法是首先设置与这些 Makefile 变量同名的环境变量并将它们 export 为全局,然后运行 configure 脚本,大多数 configure 脚本会使用这同名的环境变量代替 Makefile 中的值。但是少数 configure 脚本并不这样做(比如GCC-3.4.6和Binutils-2.16.1的脚本就不传递LDFLAGS),你必须手动编辑生成的 Makefile 文件,在其中寻找这些变量并修改它们的值,许多源码包在每个子文件夹中都有 Makefile 文件,真是一件很累人的事!

CC 与 CXX

这是 C 与 C++ 编译器命令。默认值一般是 “gcc” 与 “g++”。这个变量本来与优化没有关系,但是有些人因为担心软件包不遵守那些约定俗成的规范,害怕自己苦心设置的 CFLAGS/CXXFLAGS/LDFLAGS 之类的变量被忽略了,而索性将原本应当放置在其它变量中的选项一股老儿塞到 CC 或 CXX 中,比如:CC=”gcc -march=k8 -O2 -s”。这是一种怪异的用法,本文不提倡这种做法,而是提倡按照变量本来的含义使用变量。

CPPFLAGS

这是用于预处理阶段的选项。不过能够用于此变量的选项,看不出有哪个与优化相关。如果你实在想设一个,那就使用下面这两个吧:

-DNDEBUG

“NDEBUG”是一个标准的 ANSI 宏,表示不进行调试编译。

-D_FILE_OFFSET_BITS=64

大多数包使用这个来提供大文件(>2G)支持。
CFLAGS 与 CXXFLAGS

CFLAGS 表示用于 C 编译器的选项,CXXFLAGS 表示用于 C++ 编译器的选项。这两个变量实际上涵盖了编译和汇编两个步骤。大多数程序和库在编译时默认的优化级别是”2″(使用”-O2″选项)并且带有调试符号来编译,也就是 CFLAGS=”-O2 -g”, CXXFLAGS=$CFLAGS 。事实上,”-O2″已经启用绝大多数安全的优化选项了。另一方面,由于大部分选项可以同时用于这两个变量,所以仅在最后讲述只能用于其中一个变量的选项。[提醒]下面所列选项皆为非默认选项,你只要按需添加即可。

先说说”-O3″在”-O2″基础上增加的几项:

-finline-functions

允许编译器选择某些简单的函数在其被调用处展开,比较安全的选项,特别是在CPU二级缓存较大时建议使用。

-funswitch-loops

将循环体中不改变值的变量移动到循环体之外。

-fgcse-after-reload

为了清除多余的溢出,在重载之后执行一个额外的载入消除步骤。

另外:

-fomit-frame-pointer

对于不需要栈指针的函数就不在寄存器中保存指针,因此可以忽略存储和检索地址的代码,同时对许多函数提供一个额外的寄存器。所有”-O”级别都打开它,但仅在调试器可以不依靠栈指针运行时才有效。在AMD64平台上此选项默认打开,但是在x86平台上则默认关闭。建议显式的设置它。

-falign-functions=N
-falign-jumps=N
-falign-loops=N
-falign-labels=N

这四个对齐选项在”-O2″中打开,其中的根据不同的平台N使用不同的默认值。如果你想指定不同于默认值的N,也可以单独指定。比如,对于L2-cache>=1M的cpu而言,指定 -falign-functions=64 可能会获得更好的性能。建议在指定了 -march 的时候不明确指定这里的值。

调试选项:

-fprofile-arcs

在使用这一选项编译程序并运行它以创建包含每个代码块的执行次数的文件后,程序可以再次使用 -fbranch-probabilities 编译,文件中的信息可以用来优化那些经常选取的分支。如果没有这些信息,gcc将猜测哪个分支将被经常运行以进行优化。这类优化信息将会存放在一个以源文件为名字的并以”.da”为后缀的文件中。

全局选项:

-pipe

在编译过程的不同阶段之间使用管道而非临时文件进行通信,可以加快编译速度。建议使用。

目录选项:

–sysroot=dir

将dir作为逻辑根目录。比如编译器通常会在 /usr/include 和 /usr/lib 中搜索头文件和库,使用这个选项后将在 dir/usr/include 和 dir/usr/lib 目录中搜索。如果使用这个选项的同时又使用了 -isysroot 选项,则此选项仅作用于库文件的搜索路径,而 -isysroot 选项将作用于头文件的搜索路径。这个选项与优化无关,但是在 CLFS 中有着神奇的作用。

代码生成选项:

-fno-bounds-check

关闭所有对数组访问的边界检查。该选项将提高数组索引的性能,但当超出数组边界时,可能会造成不可接受的行为。

-freg-struct-return

如果struct和union足够小就通过寄存器返回,这将提高较小结构的效率。如果不够小,无法容纳在一个寄存器中,将使用内存返回。建议仅在完全使用GCC编译的系统上才使用。

-fpic

生成可用于共享库的位置独立代码。所有的内部寻址均通过全局偏移表完成。要确定一个地址,需要将代码自身的内存位置作为表中一项插入。该选项产生可以在共享库中存放并从中加载的目标模块。

-fstack-check

为防止程序栈溢出而进行必要的检测,仅在多线程环境中运行时才可能需要它。

-fvisibility=hidden

设置默认的ELF镜像中符号的可见性为隐藏。使用这个特性可以非常充分的提高连接和加载共享库的性能,生成更加优化的代码,提供近乎完美的API输出和防止符号碰撞。我们强烈建议你在编译任何共享库的时候使用该选项。参见 -fvisibility-inlines-hidden 选项。

硬件体系结构相关选项[仅仅针对x86与x86_64]:

-march=cpu-type

为特定的cpu-type编译二进制代码(不能在更低级别的cpu上运行)。Intel可以用:pentium2, pentium3(=pentium3m), pentium4(=pentium4m), pentium-m, prescott, nocona, core2(GCC-4.3新增) 。AMD可以用:k6-2(=k6-3), athlon(=athlon-tbird), athlon-xp(=athlon-mp), k8(=opteron=athlon64=athlon-fx)

-mfpmath=sse

P3和athlon-xp级别及以上的cpu支持”sse”标量浮点指令。仅建议在P4和K8以上级别的处理器上使用该选项。

-malign-double

将double, long double, long long对齐于双字节边界上;有助于生成更高速的代码,但是程序的尺寸会变大,并且不能与未使用该选项编译的程序一起工作。

-m128bit-long-double

指定long double为128位,pentium以上的cpu更喜欢这种标准,并且符合x86-64的ABI标准,但是却不附合i386的ABI标准。

-mregparm=N

指定用于传递整数参数的寄存器数目(默认不使用寄存器)。0<=N<=3 ;注意:当N>0时你必须使用同一参数重新构建所有的模块,包括所有的库。

-msseregparm

使用SSE寄存器传递float和double参数和返回值。注意:当你使用了这个选项以后,你必须使用同一参数重新构建所有的模块,包括所有的库。

-mmmx
-msse
-msse2
-msse3
-m3dnow
-mssse3(没写错!GCC-4.3新增)
-msse4.1(GCC-4.3新增)
-msse4.2(GCC-4.3新增)
-msse4(含4.1和4.2,GCC-4.3新增)

是否使用相应的扩展指令集以及内置函数,按照自己的cpu选择吧!

-maccumulate-outgoing-args

指定在函数引导段中计算输出参数所需最大空间,这在大部分现代cpu中是较快的方法;缺点是会明显增加二进制文件尺寸。

-mthreads

支持Mingw32的线程安全异常处理。对于依赖于线程安全异常处理的程序,必须启用这个选项。使用这个选项时会定义”-D_MT”,它将包含使用选项”-lmingwthrd”连接的一个特殊的线程辅助库,用于为每个线程清理异常处理数据。

-minline-all-stringops

默认时GCC只将确定目的地会被对齐在至少4字节边界的字符串操作内联进程序代码。该选项启用更多的内联并且增加二进制文件的体积,但是可以提升依赖于高速 memcpy, strlen, memset 操作的程序的性能。

-minline-stringops-dynamically

GCC-4.3新增。对未知尺寸字符串的小块操作使用内联代码,而对大块操作仍然调用库函数,这是比”-minline-all-stringops”更聪明的策略。决定策略的算法可以通过”-mstringop-strategy”控制。

-momit-leaf-frame-pointer

不为叶子函数在寄存器中保存栈指针,这样可以节省寄存器,但是将会使调试变的困难。注意:不要与 -fomit-frame-pointer 同时使用,因为会造成代码效率低下。

-m64

生成专门运行于64位环境的代码,不能运行于32位环境,仅用于x86_64[含EMT64]环境。

-mcmodel=small

[默认值]程序和它的符号必须位于2GB以下的地址空间。指针仍然是64位。程序可以静态连接也可以动态连接。仅用于x86_64[含EMT64]环境。

-mcmodel=kernel

内核运行于2GB地址空间之外。在编译linux内核时必须使用该选项!仅用于x86_64[含EMT64]环境。

-mcmodel=medium

程序必须位于2GB以下的地址空间,但是它的符号可以位于任何地址空间。程序可以静态连接也可以动态连接。注意:共享库不能使用这个选项编译!仅用于x86_64[含EMT64]环境。

其它优化选项:

-fforce-addr

必须将地址复制到寄存器中才能对他们进行运算。由于所需地址通常在前面已经加载到寄存器中了,所以这个选项可以改进代码。

-finline-limit=n

对伪指令数超过n的函数,编译程序将不进行内联展开,默认为600。增大此值将增加编译时间和编译内存用量并且生成的二进制文件体积也会变大,此值不宜太大。

-fmerge-all-constants

试图将跨编译单元的所有常量值和数组合并在一个副本中。但是标准C/C++要求每个变量都必须有不同的存储位置,所以该选项可能会导致某些不兼容的行为。

-fgcse-sm

在全局公共子表达式消除之后运行存储移动,以试图将存储移出循环。gcc-3.4中曾属于”-O2″级别的选项。

-fgcse-las

在全局公共子表达式消除之后消除多余的在存储到同一存储区域之后的加载操作。gcc-3.4中曾属于”-O2″级别的选项。

-floop-optimize

已废除(GCC-4.1曾包含在”-O1″中)。

-floop-optimize2

使用改进版本的循环优化器代替原来”-floop-optimize”。该优化器将使用不同的选项(-funroll-loops, -fpeel-loops, -funswitch-loops, -ftree-loop-im)分别控制循环优化的不同方面。目前这个新版本的优化器尚在开发中,并且生成的代码质量并不比以前的版本高。已废除,仅存在于GCC-4.1之前的版本中。

-funsafe-loop-optimizations

假定循环不会溢出,并且循环的退出条件不是无穷。这将可以在一个比较广的范围内进行循环优化,即使优化器自己也不能断定这样做是否正确。

-fsched-spec-load

允许一些装载指令执行一些投机性的动作。

-ftree-loop-linear

在trees上进行线型循环转换。它能够改进缓冲性能并且允许进行更进一步的循环优化。

-fivopts

在trees上执行归纳变量优化。

-ftree-vectorize

在trees上执行循环向量化。

-ftracer

执行尾部复制以扩大超级块的尺寸,它简化了函数控制流,从而允许其它的优化措施做的更好。据说挺有效。

-funroll-loops

仅对循环次数能够在编译时或运行时确定的循环进行展开,生成的代码尺寸将变大,执行速度可能变快也可能变慢。

-fprefetch-loop-arrays

生成数组预读取指令,对于使用巨大数组的程序可以加快代码执行速度,适合数据库相关的大型软件等。具体效果如何取决于代码。

-fweb

建立经常使用的缓存器网络,提供更佳的缓存器使用率。gcc-3.4中曾属于”-O3″级别的选项。

-ffast-math

违反IEEE/ANSI标准以提高浮点数计算速度,是个危险的选项,仅在编译不需要严格遵守IEEE规范且浮点计算密集的程序考虑采用。

-fsingle-precision-constant

将浮点常量作为单精度常量对待,而不是隐式地将其转换为双精度。

-fbranch-probabilities

在使用 -fprofile-arcs 选项编译程序并执行它来创建包含每个代码块执行次数的文件之后,程序可以利用这一选项再次编译,文件中所产生的信息将被用来优化那些经常发生的分支代码。如果没有这些信息,gcc将猜测那一分支可能经常发生并进行优化。这类优化信息将会存放在一个以源文件为名字的并以”.da”为后缀的文件中。

-frename-registers

试图驱除代码中的假依赖关系,这个选项对具有大量寄存器的机器很有效。gcc-3.4中曾属于”-O3″级别的选项。

-fbranch-target-load-optimize
-fbranch-target-load-optimize2

在执行序启动以及结尾之前执行分支目标缓存器加载最佳化。

-fstack-protector

在关键函数的堆栈中设置保护值。在返回地址和返回值之前,都将验证这个保护值。如果出现了缓冲区溢出,保护值不再匹配,程序就会退出。程序每次运行,保护值都是随机的,因此不会被远程猜出。

-fstack-protector-all

同上,但是在所有函数的堆栈中设置保护值。

–param max-gcse-memory=xxM

执行GCSE优化使用的最大内存量(xxM),太小将使该优化无法进行,默认为50M。

–param max-gcse-passes=n

执行GCSE优化的最大迭代次数,默认为 1。

传递给汇编器的选项:

-Wa,options

options是一个或多个由逗号分隔的可以传递给汇编器的选项列表。其中的每一个均可作为命令行选项传递给汇编器。

-Wa,–strip-local-absolute

从输出符号表中移除局部绝对符号。

-Wa,-R

合并数据段和正文段,因为不必在数据段和代码段之间转移,所以它可能会产生更短的地址移动。

-Wa,–64

设置字长为64bit,仅用于x86_64,并且仅对ELF格式的目标文件有效。此外,还需要使用”–enable-64-bit-bfd”选项编译的BFD支持。

-Wa,-march=CPU

按照特定的CPU进行优化:pentiumiii, pentium4, prescott, nocona, core, core2; athlon, sledgehammer, opteron, k8 。

仅可用于 CFLAGS 的选项:

-fhosted

按宿主环境编译,其中需要有完整的标准库,入口必须是main()函数且具有int型的返回值。内核以外几乎所有的程序都是如此。该选项隐含设置了 -fbuiltin,且与 -fno-freestanding 等价。

-ffreestanding

按独立环境编译,该环境可以没有标准库,且对main()函数没有要求。最典型的例子就是操作系统内核。该选项隐含设置了 -fno-builtin,且与 -fno-hosted 等价。

仅可用于 CXXFLAGS 的选项:

-fno-enforce-eh-specs

C++标准要求强制检查异常违例,但是该选项可以关闭违例检查,从而减小生成代码的体积。该选项类似于定义了”NDEBUG”宏。

-fno-rtti

如果没有使用’dynamic_cast’和’typeid’,可以使用这个选项禁止为包含虚方法的类生成运行时表示代码,从而节约空间。此选项对于异常处理无效(仍然按需生成rtti代码)。

-ftemplate-depth-n

将最大模版实例化深度设为’n’,符合标准的程序不能超过17,默认值为500。

-fno-optional-diags

禁止输出诊断消息,C++标准并不需要这些消息。

-fno-threadsafe-statics

GCC自动在访问C++局部静态变量的代码上加锁,以保证线程安全。如果你不需要线程安全,可以使用这个选项。

-fvisibility-inlines-hidden

默认隐藏所有内联函数,从而减小导出符号表的大小,既能缩减文件的大小,还能提高运行性能,我们强烈建议你在编译任何共享库的时候使用该选项。参见 -fvisibility=hidden 选项。
LDFLAGS

LDFLAGS 是传递给连接器的选项。这是一个常被忽视的变量,事实上它对优化的影响也是很明显的。

[提示]以下选项是在完整的阅读了ld-2.18文档之后挑选出来的选项。http://blog.chinaunix.net/u1/41220/showart_354602.html 有2.14版本的中文手册。

-s

删除可执行程序中的所有符号表和所有重定位信息。其结果与运行命令 strip 所达到的效果相同,这个选项是比较安全的。

-Wl,options

options是由一个或多个逗号分隔的传递给链接器的选项列表。其中的每一个选项均会作为命令行选项提供给链接器。

-Wl,-On

当n>0时将会优化输出,但是会明显增加连接操作的时间,这个选项是比较安全的。

-Wl,–exclude-libs=ALL

不自动导出库中的符号,也就是默认将库中的符号隐藏。

-Wl,-m<emulation>

仿真<emulation>连接器,当前ld所有可用的仿真可以通过”ld -V”命令获取。默认值取决于ld的编译时配置。

-Wl,–sort-common

把全局公共符号按照大小排序后放到适当的输出节,以防止符号间因为排布限制而出现间隙。

-Wl,-x

删除所有的本地符号。

-Wl,-X

删除所有的临时本地符号。对于大多数目标平台,就是所有的名字以’L’开头的本地符号。

-Wl,-zcomberloc

组合多个重定位节并重新排布它们,以便让动态符号可以被缓存。

-Wl,–enable-new-dtags

在ELF中创建新式的”dynamic tags”,但在老式的ELF系统上无法识别。

-Wl,–as-needed

移除不必要的符号引用,仅在实际需要的时候才连接,可以生成更高效的代码。

-Wl,–no-define-common

限制对普通符号的地址分配。该选项允许那些从共享库中引用的普通符号只在主程序中被分配地址。这会消除在共享库中的无用的副本的空间,同时也防止了在有多个指定了搜索路径的动态模块在进行运行时符号解析时引起的混乱。

-Wl,–hash-style=gnu

使用gnu风格的符号散列表格式。它的动态链接性能比传统的sysv风格(默认)有较大提升,但是它生成的可执行程序和库与旧的Glibc以及动态链接器不兼容。

最后说两个与优化无关的系统环境变量,因为会影响GCC编译程序的方式,下面两个是咱中国人比较关心的:

LANG

指定编译程序使用的字符集,可用于创建宽字符文件、串文字、注释;默认为英文。[目前只支持日文”C-JIS,C-SJIS,C-EUCJP”,不支持中文]

LC_ALL

指定多字节字符的字符分类,主要用于确定字符串的字符边界以及编译程序使用何种语言发出诊断消息;默认设置与LANG相同。中文相关的几项:”zh_CN.GB2312 , zh_CN.GB18030 , zh_CN.GBK , zh_CN.UTF-8 , zh_TW.BIG5″。
最后编辑:
作者:wy182000
这个作者貌似有点懒,什么都没有留下。

留下一个回复