[zz]GCC入门详解

一下子想不起GCC查看执行文件的汇编码方法,晚上回去翻翻CS:APP

作为自由软件的旗舰项目,Richard Stallman 在十多年前刚开始写作 GCC 的时候,还只是把它当作仅仅一个 C 程序语言的编译器;GCC 的意思也只是 GNU C Compiler 而已。经过了这么多年的发展,GCC 已经不仅仅能支持 C 语言;它现在还支持 Ada 语言、C++ 语言、Java 语言、Objective C 语言、Pascal 语言、COBOL语言,以及支持函数式编程和逻辑编程的 Mercury 语言,等等。而 GCC 也不再单只是 GNU C 语言编译器的意思了,而是变成了 GNU Compiler Collection 也即是 GNU 编译器家族的意思了。另一方面,说到 GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。

1 程序编译过程


  GCC是CUI(命令行交互界面)程序,这让许多从Windows走出来 Guier们感到恐惧。实际上它也有许多前端窗口界面,Windows下有Dev C++,Linux下譬如KDevelopment,但既然选择了GCC还是将CUL进行到底吧,没有难与不难的问题,只有做与不做的问题!

  下面基于一个具体而微的程序,讨论GCC的使用。示例程序如下:

//test.c
#include <stdio.h>
int main(void)
{
  printf(“Hello World!\n”);
  return 0;
}

  这个程序,一步到位的编译指令是:

gcc test.c -o test

  输出的可执行文件名为test,Windows用户可能会感到奇怪,可执行文件明怎么没有.exe扩展名呢?Linux系统中,文件类型并非以扩展名识别的!

  实质上,上述编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编 (Assembly)和连接(Linking)。

1.1 预处理

  运行预处理命令:

gcc -E test.c -o test.i  gcc -E test.c

可以输出test.i文件中存放着test.c经预处理之后的代码。打开test.i文件,看一看,就明白了。后面那条指令,是直接在命令行窗口中输出预处理后的代码,而不是以文件作为输出设备。gcc的-E选项,可以让编译器在预处理后停止,并输出预处理结果。在本例中,预处理结果就是将stdio.h 文件中的内容插入到test.c中了。

  gcc的-o选项,用于输出处理结果到文件中。

1.2 编译为汇编代码

  预处理之后,可直接对生成的test.i文件编译,生成汇编代码:

gcc -S test.i -o test.s

  gcc的-S选项,表示在程序编译期间,在生成汇编代码后,停止,-o输出汇编代码文件。

  生成的汇编代码如下:

    .file    “test.c”
    .section    .rodata
    .align 4
.LC0:
    .string    “Hello World,Linux programming!”
    .text
.globl main
    .type    main, @function
main:
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl    -4(%ecx)
    pushl    %ebp
    movl    %esp, %ebp
    pushl    %ecx
    subl    $4, %esp
    movl    $.LC0, (%esp)
    call    puts
    movl    $0, %eax
    addl    $4, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret
    .size    main, .-main
    .ident    “GCC: (GNU) 4.1.0 20060304 (Red Hat 4.1.0-3)”
    .section    .note.GNU-stack,””,@progbits

1.3 汇编(Assembly)

  如果你学过汇编语言,那么你就该知道程序编译到了这个地步,应当使用汇编器,将汇编语言翻译为机器代码了。这一步尤其重要,因为它决定了你生成的程序,能够运行在哪种机器上。gcc使用的汇编器是gas。

  在Intel IA-32平台上,还有一些常用的汇编器有:

  • 微软的MASM,这是Intel平台上所有汇编器的鼻祖了,它现在已不是微软的独立产品,只是与Visual Studio捆绑在一起。但微软允许其他组织免费分发MASM 6.0。
  • NASM,最初是为UNIX环境开发的商业汇编器,最近成为开源的了,可生成UNIX、MS-DOS和32位Windows格式的可执行文件。
  • HLA(high level assembler)是Randall Hyde教授创建的,可以在DOS、Windows和Linux操作系统上生成Intel指令码。但HLA设计的主要目的是向初级程序员讲授汇编语言,学院气太浓,不够实用。

  与这些汇编器相比,gas可以在不同处理器平台上工作,通常它可以自动检测底层硬件平台并生成适合该平台的正确机器指令码。gas另一个特性是能够创建不同于程序设计所在平台的指令码,譬如我在Intel计算机上工作,但可以为MIPS计算机写程序。

  对于上一小节中生成的汇编代码文件test.s,gas汇编器负责将其编译为目标文件,如下:

gcc -c test.s -o test.o

1.4 连接

  gcc连接器是gas提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。

  对于上一小节中生成的test.o,将其与C标准输入输出库进行连接,最终生成程序test:

gcc test.o -o test

  在命令行窗口中,运行test这个小程序,让它说HelloWorld吧!

2、多个程序文件的编译



  通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用GCC能够很好地管理这些编译单元。假设有一个由test1.c和 test2.c两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序test,可以使用下面这条命令:

# gcc test1.c test2.c -o test

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

# gcc -c test1.c -o test1.o
# gcc -c test2.c -o test2.o
# gcc test1.o test2.o -o test

  需要打这么多编译指令,看着都累,许多Guier们又要抱怨了。的确如此,如果单单使用GCC来编译你的程序,一千个程序源文件的项目编译至少要在命令行窗口中敲1k次文件名,才能完成一次编译。如果代码有了改动,重新编译,需要再原样输入一次编译指令。再技术高超的Cler也会累死的,但是很奇怪,那些Cler们至今依然活的很生龙活虎,这得益于GNU Make工具,详情见Make基础一节。

3、检错



  GCC包含完整的出错检查和警告提示功能,可以帮助程序员写出更为标准、健壮的代码。如下面的代码:

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

  这种代码,可能在老的C语言课本里能够见到,但它是不符合ANSI/ISO C语言标准的。我让同学在Visual Stdio .net 2003上编译了一下,没检测出什么问题来。下面看看GCC可不可以:

gcc -pedantic illcode.c -o illcode

  输出结果:

illcode.c: 在函数 ‘main’ 中:
illcode.c:5: 警告:ISO C90 不支持 ‘long long’
illcode.c:4: 警告:‘main’ 的返回类型不是 ‘int’

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

  如果采用默认的编译,即:gcc -pedantic illcode.c -o illcode。输出:

test.c: 在函数 ‘main’ 中:
test.c:4: 警告:‘main’ 的返回类型不是 ‘int’

  上面的示例中,long long int是GNU C的扩展类型,表示64位整型数,这种类型没有纳入C/C++标准中,可见GCC默认的编译指令,无法完全检测出不符合标准C/C++的代码,但要比 Visual Stdio .net 2003一声都不吭要好一些。如果使用-pedantic选项,GCC就可以基本上按照标准C/C++进行代码检测了,不要挑剔什么,迄今为止没有任何一款编译器完全支持标准C/C++的。
  
  除了-pedantic之外,GCC还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W开头,其中最有价值的当数-Wall了,使用它能够使GCC产生尽可能多的警告信息。

  GCC给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持标准、健壮的特性。所以将警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror选项,那么GCC会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下:

gcc -Werror test.c -o test

 
  输出:

cc1: warnings being treated as errors
test.c: 在函数 ‘main’ 中:
test.c:4: 警告:‘main’ 的返回类型不是 ‘int’

4、库文件连接



  人家已经发明了轮子,而且物美价廉,那么我们就实在没有必要浪费生命再去发明同样的轮子!开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助许多函数库的支持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、或lib、dll)的集合。虽然Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录下;Windows所使用的库文件主要放在Visual Stido的目录下的include和lib,以及系统文件夹下。但也有的时候,我们要用的库不再这些目录下,所以GCC在编译时必须用自己的办法来查找所需要的头文件和库文件。

  GCC采用搜索目录的办法来查找所需要的文件,-I选项可以向GCC的头文件搜索路径中添加新的目录。例如,如果在 /home/lyanry/include/目录下有编译时所需要的头文件,为了让GCC能够顺利地找到它们,就可以使用-I选项:

# gcc test.c -I /home/lyanry/include -o test

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

# gcc test.c -L /home/lyanry/lib -ltest -o test


  上面这条命令中,值得好好解释一下的是-l选项,它指示GCC去连接库文件libfoo.so。Linux下的库文件在命名时有一个约定,那就是应该以lib三个字母开头,由于所有的库文件都遵循了同样的规范,因此在用-l选项指定链接的库文件名时可以省去lib三个字母,也就是说GCC在对-lfoo进行处理时,会自动去链接名为libfoo.so的文件。(注:至于在Windows下该怎样连接库文件,未做尝试,以后再谈)

  Linux下的库文件分为两大类分别是动态链接库(通常以.so结尾)和静态链接库(通常以.a结尾),二者的区别仅在于程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的。动态加载,意味着内存中仅存在一份库代码,所调用的函数只是在调用程序中存在一个映像。而静态加载,意味着将库中所调用的函数代码复制到调用程序中。如果库中存在同名的静态库和动态库,则在默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。例如,如果在 /home/xiaowp/lib/目录下有链接时所需要的库文件libtest.so和libtest.a,为了让GCC在链接时只用到静态链接库,可以使用下面的命令:

# gcc test.c -L /home/xiaowp/lib -static -ltest -o test

5、优化



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

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

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

//testOpt.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 + 2000; counter += (5 – 1) / 4)
  {
    temp = counter / 1979;
    result = counter;
  }
  printf(“Result is %lf\n”, result);
  return 0;
}

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

gcc -Wall testOpt.c -o testOpt

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

$time ./testOpt
Result is 400001999.000000

real    0m7.759s
user    0m7.444s
sys     0m0.008s

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

gcc -Wall -O testOpt.c -o testOpt

  测试运行时间:

$time ./testOPt
Result is 400001999.000000

real    0m2.445s
user    0m2.436s
sys     0m0.000s

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

gcc -Wall -O2 testOpt.c -o testOpt

  测试运行时间:

$time ./testOPt
Result is 400001999.000000

real    0m2.338s
user    0m2.320s
sys     0m0.004s

  尽管GCC的代码优化功能非常强大,但作为一名优秀的Linux程序员,首先还是要力求能够手工编写出高质量的代码。如果编写的代码简短,并且逻辑性强,编译器就不会做更多的工作,甚至根本用不着优化。特别在以下一些场合中应该避免使用优化:

  1. 程序开发的时候优化等级越高,消耗在编译上的时间就越长,因此在开发的时候最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的代码进行优化。
  2. 资源受限的时候一些优化选项会增加可执行代码的体积,如果程序在运行时能够申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,因为由这带来的负面影响可能会产生非常严重的后果。
  3. 跟踪调试的时候对代码进行优化,容易导致某些代码可能会被删除或改写,或者为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难。

6、程序性能分析 


 GCC支持的其它调试选项还包括-p和-pg,它们会将剖析(Profiling)信息加入到最终生成的二进制代码中。剖析信息即包含了更为详细的调试信息(只是我这么觉得,由下面的例子可以证实),也对于找出程序的性能瓶颈很有帮助,是协助Linux程序员开发出高性能程序的有力工具。在编译时加入-p选项会在生成的代码中加入通用剖析工具(Prof)能够识别的统计信息,而 -pg选项则生成只有GNU剖析工具(Gprof)才能识别的统计信息。下面我们还是以crash.c程序的编译和调试,来看看使用-p选项对程序调试的好处吧。

  编译:

 gcc -Wall -g -p crash.c -o crash

  调试:

[lyanry@lyanry crash]$ gdb -q crash
Using host libthread_db library “/lib/libthread_db.so.1”.
(gdb) run
Starting program: /home/lyanry/program/c++/crash/crash
Reading symbols from shared object read from target memory…done.
Loaded system supplied DSO at 0x909000
Input an integer:11

Program received signal SIGSEGV, Segmentation fault.
0x00971667 in _IO_vfscanf_internal () from /lib/libc.so.6
(gdb) backtrace
#0  0x00971667 in _IO_vfscanf_internal () from /lib/libc.so.6
#1  0x00979337 in scanf () from /lib/libc.so.6
#2  0x08048520 in main () at crash.c:8

  现在,可以从GDB输出结果中看到带有出错代码行号的backtrace结果了,即#2  0x08048520 in main () at crash.c:8,使用frame指令,查看出错代码,结果如下:

(gdb) frame 2
#2  0x08048520 in main () at crash.c:8
8         scanf(“%d”, input);

  现在有点清晰地知道问题发生在哪了吧!

  下面,来测试-p或-pg选项用于分析程序的性能瓶颈,结合前面的叙述,看一下man手册上对-p和-pg选项的详细说明:

-p  Generate extra code to write profile information suitable for the
    analysis program prof.  You must use this option when compiling the
    source files you want data about, and you must also use it when
    linking.

-pg Generate extra code to write profile information suitable for the
    analysis program gprof.  You must use this option when compiling
    the source files you want data about, and you must also use it when
    linking.

  
  说明中所提及的prof和gprof,都是程序性能剖析工具,prof是通用的,gprof是GNU开发的。以例程profile.c来测试 gprof,在编译程序时要添加-gp选项。要注意,这个选项只是在连接期间产生作用的。profile.c代码清单如下:

//profile.c
#include <stdio.h>
void function1()
{
  int i=0,j;
  for(j=0;j<100000;j++)
    i+=j;
}
void function2()
{
  int i,j;
  function1();
  for(j=0;j<200000;j++)
    i=j;
}
int main(void)
{
  int i,j;
  for(i=0;i<100;i++)
    function1();
  for(j=0;i<50000;i++)
    function2();

  return 0;
}

  编译:

gcc -Wall -pg profile.c -o profile


  运行profile程序,会在当前目录中生成一个gmon.out文件,下面可以使用gprof工具对profile程序进行剖析了:

gprof profile >gprof.txt

  上面指令执行时,gprof会自动使用gmon.out文件,将输出结果重定向到gprof.txt文件中。如果想知道gmon.out是什么,还是看看man手册里的描述吧:

 “Gprof” reads the given object file (the default is “a.out”) and establishes the relation between its symbol table and  the  call  graph profile from gmon.out. 

  
  好了,现在要做的事情,就是在当前目录下打开gprof.txt,看看了,文件中,我们感兴趣的内容通常有两处:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total          
 time   seconds   seconds    calls  us/call  us/call  name   
 66.92     24.44    24.44    49900   489.78   731.38  function2
 33.08     36.52    12.08    50000   241.60   241.60  function1

  与

index % time    self  children    called     name
                                                 <spontaneous>
[1]    100.0    0.00   36.52                 main [1]
               24.44   12.06   49900/49900       function2 [2]
                0.02    0.00     100/50000       function1 [3]
———————————————–
               24.44   12.06   49900/49900       main [1]
[2]     99.9   24.44   12.06   49900         function2 [2]
               12.06    0.00   49900/50000       function1 [3]
———————————————–
                0.02    0.00     100/50000       main [1]
               12.06    0.00   49900/50000       function2 [2]
[3]     33.1   12.08    0.00   50000         function1 [3]
———————————————–

  
  不想再细说下去,自己琢磨去吧。

       g++ -g MyFirst.cpp -o MyFirst

——————————————————————————————————

gcc和g++都是GNU(组织)的一个编译器。

误区一:gcc只能编译c代码,g++只能编译c++代码
两者都可以,但是请注意:
1.后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序;后缀为.cpp的,两者都会认为是c++程序,注意,虽然c++是c的超集,但是两者对语法的要求是有区别的。C++的语法规则更加严谨一些。
2.编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接统统用g++了,这就给人一种错觉,好像cpp程序只能用g++似的。
 
误区二:gcc不会定义__cplusplus宏,而g++会
实际上,这个宏只是标志着编译器将会把代码按C还是C++语法来解释,如上所述,如果后缀为.c,并且采用gcc编译器,则该宏就是未定义的,否则,就是已定义。
 
误区三:编译只能用gcc,链接只能用g++
严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接,所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。

gcc和g++的区别
我们在编译c/c++代码的时候,有人用gcc,有人用g++,于是各种说法都来了,譬如c代码用gcc,而c++代码用g++,或者说编译用gcc,链接用g++,一时也不知哪个说法正确,如果再遇上个extern “C”,分歧就更多了,这里我想作个了结,毕竟知识的目的是令人更清醒,而不是更糊涂。

 
误区一:gcc只能编译c代码,g++只能编译c++代码
两者都可以,但是请注意:
1.后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序;后缀为.cpp的,两者都会认为是c++程序,注意,虽然c++是c的超集,但是两者对语法的要求是有区别的,例如:
#include <stdio.h>
int main(int argc, char* argv[]) {
   if(argv == 0) return;
   printString(argv);
   return;
}
int printString(char* string) {
  sprintf(string, “This is a test.\n”);
}
如果按照C的语法规则,OK,没问题,但是,一旦把后缀改为cpp,立刻报三个错:“printString未定义”;
“cannot convert `char**’ to `char*”;
”return-statement with no value“;
分别对应前面红色标注的部分。可见C++的语法规则更加严谨一些。
2.编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接,为了统一起见,干脆编译/链接统统用g++了,这就给人一种错觉,好像cpp程序只能用g++似的。
 
误区二:gcc不会定义__cplusplus宏,而g++会
实际上,这个宏只是标志着编译器将会把代码按C还是C++语法来解释,如上所述,如果后缀为.c,并且采用gcc编译器,则该宏就是未定义的,否则,就是已定义。
 
误区三:编译只能用gcc,链接只能用g++

严格来说,这句话不算错误,但是它混淆了概念,应该这样说:编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接,所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。
 
误区四:extern “C”与gcc/g++有关系
实际上并无关系,无论是gcc还是g++,用extern “c”时,都是以C的命名方式来为symbol命名,否则,都以c++方式命名。试验如下:
me.h
extern “C” void CppPrintf(void);
 
me.cpp:
#include <iostream>
#include “me.h”
using namespace std;
void CppPrintf(void)
{
     cout << “Hello\n”;
}
 
test.cpp:
#include <stdlib.h>
#include <stdio.h>
#include “me.h”        
int main(void)
{
    CppPrintf();
    return 0;
}
 
1. 先给me.h加上extern “C”,看用gcc和g++命名有什么不同
[root@root G++]# g++ -S me.cpp
[root@root G++]# less me.s
.globl _Z9CppPrintfv        //注意此函数的命名
        .type   CppPrintf, @function
[root@root GCC]# gcc -S me.cpp
[root@root GCC]# less me.s
.globl _Z9CppPrintfv        //注意此函数的命名
        .type   CppPrintf, @function
完全相同!
               
2. 去掉me.h中extern “C”,看用gcc和g++命名有什么不同

[root@root GCC]# gcc -S me.cpp
[root@root GCC]# less me.s
.globl _Z9CppPrintfv        //注意此函数的命名
        .type   _Z9CppPrintfv, @function
[root@root G++]# g++ -S me.cpp
[root@root G++]# less me.s
.globl _Z9CppPrintfv        //注意此函数的命名
        .type   _Z9CppPrintfv, @function
完全相同!
【结论】完全相同,可见extern “C”与采用gcc/g++并无关系,以上的试验还间接的印证了前面的说法:在编译阶段,g++是调用gcc的。

posted on 2008-04-20 22:20 exce4 阅读(75) 评论(2)  编辑 收藏


评论

#1楼 [楼主] 2008-04-20 22:24 exce4

前言
网上关于编译优化的文章很多,但大多零零散散,不成体系,本文试图给出一个完整和清晰的优化思路,同时提供在实践中如何进行优化的详尽参考。但是,在介绍所有优化知识之前首先引用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 ,然后再来阅读此文将会更有意义。另外,本文是建立在《深入理解软件包的配置、编译与安装》一文基础之上的,在开始阅读本文之前,请先阅读它。

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

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

编译工具的选择
对于编译工具自身的选择,在假定使用 Binutils 和 GCC 以及 Make 的前提下,没什么好说的,基本上新版本都能带来性能提升,同时比老版本对新硬件的支持更好,所以应当尽量选用新版本。不过追新也可能带来系统的不稳定,这就要针对实际情况进行权衡了。本文以 Binutils-2.17 和 GCC-4.1.1 以及 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 脚本并不这样做,你必须手动编辑生成的 Makefile 文件,在其中寻找这些变量并修改它们的值,一些源码包在每个子文件夹中都有 Makefile 文件,真是一件很累人的事!

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

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

-DNDEBUG
“NDEBUG”是一个标准的 ANSI 宏,表示不进行调试编译。
CFLAGS 与 CXXFLAGS
CFLAGS 表示用于 C 编译器的选项,CXXFLAGS 表示用于 C++ 编译器的选项。这两个变量实际上涵盖了编译和汇编两个步骤。大多数程序和库在编译时默认的优化级别是”2″(使用”-O2″选项)并且在 Intel/AMD平台上默认按照i386处理器来编译,也就是 CFLAGS=”-O2 -march=i386″, 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 的时候不明确指定这里的值。
调试选项:

-fpretend-float
交叉编译的时候,假定目标机和宿主机使用同样的浮点格式。它导致输出错误的浮点常数,但是在目标机上运行的时候,真实的指令序列有可能和GNU CC希望的一样。
-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足够小就通过寄存器返回,这将提高较小结构的效率。如果不够小,无法容纳在一个寄存器中,将使用内存返回。这是一个比较安全的选项。
-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 。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
-mno-mmx
-msse
-mno-sse
-msse2
-mno-sse2
-msse3
-mno-sse3
-m3dnow
-mno-3dnow
是否使用相应的扩展指令集以及内置函数,按照自己的cpu选择吧!
-maccumulate-outgoing-args
指定在函数引导段中计算输出参数所需最大空间,这在大部分现代cpu中是较快的方法;缺点是会增加代码尺寸。
-mthreads
支持Mingw32的线程安全异常处理。对于依赖于线程安全异常处理的程序,必须启用这个选项。使用这个选项时会定义”-D_MT”,它将包含使用选项”-lmingwthrd”连接的一个特殊的线程辅助库,用于为每个线程清理异常处理数据。
-minline-all-stringops
默认时GCC只将确定目的地会被对齐在至少4字节边界的字符串操作内联进程序代码。该选项启用更多的内联并且增加二进制文件的体积,但是可以提升依赖于高速 memcpy, strlen, memset 操作的程序的性能。
-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-optimize2
使用改进版本的循环优化器代替原来的老版本(-floop-optimize,包含在”- O1″中)。该优化器将使用不同的选项(-funroll-loops, -fpeel-loops, -funswitch-loops, -ftree-loop-im)分别控制循环优化的不同方面。目前这个新版本的优化器尚在开发中,并且生成的代码质量并不比以前的版本高。
-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
设置字长为64。仅用于x86_64,并且仅对ELF格式的目标文件有效。
仅可用于 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 是传递给连接器的选项。这是一个常被忽视的变量,事实上它对优化的影响也是很明显的。

-s
删除可执行程序中的所有符号表和所有重定位信息。其结果与运行命令 strip 所达到的效果相同。
-static
连接器将忽略动态连接库,同时通过将静态目标文件直接包含到结果目标文件完成对所有引用的解析。
-shared
链接器将生成共享目标代码,该共享库可在运行时动态链接到程序形成完整可执行体。而且,如果使用 gcc 命令创建共享库作为其输出,该选项可以防止链接器将缺失 main() 方法视为错误。为了可以正确工作,应该一致地使用选项 -fpic 和 -fPIC 以及目标平台选项编译构成同一库的所有共享目标模块。特别是,该选项可能需要生成特殊代码来让构造函数正常工作。由于不正确的选项设定而产生的错误可能很不明显,而且也没有警告消息。
-shared-libgcc
链接共享版本的libgcc 。当应用程序需要从一个共享库代码抛出另一个共享库捕获的异常时,需要使用共享版本的libgcc 。
-static-libgcc
链接静态版本的libgcc 。该选项可能会引起 C++ 和 Java 中的异常处理问题。
-Wl,options
options是由一个或多个逗号分隔的传递给链接器的选项列表。其中的每一个选项均会作为命令行选项提供给链接器。
-Wl,-On
当n>0时将会优化输出,但是会明显增加连接操作的时间,这个选项是比较安全的。
-Wl,–sort-common
把全局公共符号按照大小排序后放到适当的输出节,以防止符号间因为排布限制而出现间隙。
-Wl,–no-keep-memory
按需读取符号表而不是将它们缓存在内存中,这样可以减少内存用量,但是会降低运行速度。仅在连接大型可执行文件时有些意义。
-Wl,-s
剥离输出文件中所有的符号信息,这个选项是比较安全的。
-Wl,-znow
默认仅在函数首次调用时才解析相应的符号,这样减少了内存用量,但是可能降低了程序启动速度。而使用这个选项可以强制动态连接器在启动时就解析所有符号,这可能导致占用更多的内存,但是程序启动速度可能加快。
-Wl,–enable-new-dtags
在ELF中创建新式的”dynamic tags”,但在老式的ELF系统上无法识别。
-Wl,–as-needed
移除不必要的符号引用,仅在实际需要的时候才连接,可以生成更高效的代码。

——————————————————————————–

最后说两个与优化无关的系统环境变量,因为会影响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″  

此条目发表在C, 编程, 网摘分类目录,贴了标签。将固定链接加入收藏夹。

发表评论

邮箱地址不会被公开。 必填项已用*标注