简介

  • gccg++分别是GNU的C和C++编译器。gccg++在执行编译工作的时候,总共四步:
    • 预处理,生成.i文件{预处理器 – cpp}
    • 将预处理后的文件转换成汇编语言,生成文件.s{编译器 – egcs}
    • 由汇编变为目标代码(机器代码)生成.o的文件{汇编器 – as}
    • 链接目标代码,生成可执行程序{链接器 – ld}
  • 参数:
    • -c:只激活预处理、编译和汇编,生成obj文件.o
    • -S:只激活预处理和编译,生成汇编代码
    • -I:指定头文件所在路径
    • -L:指定库文件所在路径
    • -l:指定库的名字
  • 注意:
    • GCC是一个驱动式的程序,它调用其他程序来依次进行编译、汇编和链接
    • GCC分析命令行参数,然后决定该调用哪一个子程序,哪些参数应该传递给子程序。所有这些行为都是由SPEC字符串(spec strings)来控制的。
    • 通常情况下,每一个GCC可以调用的子程序都对应着一个SPEC字符串,不过有少数的子程序需要多个SPEC字符串来控制他们的行为。
  • 深入理解计算机操作系统 P109
    • 试图最大化一段关键代码性能的程序员,通常会尝试源代码的各种形式,每次编译并检查产生的汇编代码,从而了解程序将要运行的效率如何
    • 此外,有些时候,高级语言提供的抽象层会隐藏我们想要了解的程序的运行时行为

GCC参数

  • -x language filename
    • 设定文件所使用的语言, 使后缀名无效, 对以后的多个有效。
    • 也就是根据约定 C 语言的后缀名称是 .c 的,而 C++ 的后缀名是 .C 或者 .cpp
    • 可以使用的参数:
      • c
      • objective-c
      • c-header
      • c++
      • cpp-output
      • assembler
      • assembler-with-cpp
    • 例如:gcc -x c hello.pig
  • -x none filename
    • 关掉上一个选项,也就是让gcc根据文件名后缀,自动识别文件类型 。
  • -pipe
    • 使用管道代替编译中临时文件, 在使用非 gnu 汇编工具的时候, 可能有些问题
    • 例如:gcc -pipe -o hello.exe hello.c
  • -ansi
    • 关闭 gnu c中与 ansi c 不兼容的特性, 激活 ansi c 的专有特性(包括禁止一些 asm inline typeof 关键字, 以及 UNIX,vax 等预处理宏)
  • -fno-asm
    • 此选项实现 ansi 选项的功能的一部分,它禁止将 asm, inlinetypeof 用作关键字。
  • -fno-strict-prototype
    • 只对 g++ 起作用,
    • 使用这个选项, g++ 将对不带参数的函数,都认为是没有显式的对参数的个数和类型说明,而不是没有参数
    • gcc 无论是否使用这个参数, 都将对没有带参数的函数, 认为没有显式说明的类型
  • -fthis-is-varialble
    • 就是向传统 c++ 看齐, 可以使用 this 当一般变量使用
  • -fcond-mismatch
    • 允许条件表达式的第二和第三参数类型不匹配, 表达式的值将为 void 类型
  • -funsigned-char-fno-signed-char-fsigned-char-fno-unsigned-char
    • 这四个参数是对 char 类型进行设置, 决定将 char 类型设置成 unsigned char(前两个参数)或者 signed char(后两个参数)。
  • -include file
    • 包含某个代码,简单来说,就是便以某个文件,需要另一个文件的时候,就可以用它设定,
    • 功能就相当于在代码中使用 #include<filename>
  • -imacros file
    • 将 file 文件的宏, 扩展到 gcc/g++ 的输入文件, 宏定义本身并不出现在输入文件中
  • -Dmacro
    • 定义宏macro
    • 相当于 C 语言中的 #define macro
  • -Umacro
    • 相当于 C 语言中的 #undef macro
  • -undef
    • 取消对任何非标准宏的定义
  • -Idir
    • 在你是用 #include "file" 的时候, gcc/g++ 会先在当前目录查找你所制定的头文件, 如果没有找到, 他回到默认的头文件目录找, 如果使用 -I 制定了目录,他会先在你所制定的目录查找, 然后再按常规的顺序去找
    • 对于 #include<file>, gcc/g++ 会到 -I 制定的目录查找, 查找不到, 然后将到系统的默认的头文件目录查找
  • -C
    • 在预处理的时候, 不删除注释信息, 一般和-E使用, 有时候分析程序,用这个很方便的
  • -Wa,option
    • 此选项传递 option 给汇编程序; 如果 option 中间有逗号, 就将 option 分成多个选项, 然 后传递给会汇编程序
  • -Wl.option
    • 此选项传递 option 给链接程序; 如果 option 中间有逗号, 就将 option 分成多个选项, 然后传递给会链接程序
  • -llibrary
    • 制定编译的时候使用的库
  • -Ldir
    • 制定编译的时候,搜索库的路径。比如你自己的库,可以用它制定目录,不然编译器将只在标准库的目录找。这个dir就是目录的名称
  • -O0 、-O1 、-O2 、-O3
    • 编译器的优化选项的 4 个级别,-O0 表示没有优化, -O1 为默认值,-O3 优化级别最高
  • -g
    • 只是编译器,在编译的时候,产生调试信息
  • -ggdb
    • 此选项将尽可能的生成 gdb 的可以使用的调试信息
  • -static
    • 此选项将禁止使用动态库,所以,编译出来的东西,一般都很大,也不需要什么动态连接库,就可以运行。
  • -share
    • 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统有动态库。
  • -traditional
    • 试图让编译器支持传统的C语言特性。
    • GCCGNUCC++ 编译器。实际上,GCC 能够编译三种语言:CC++Object CC 语言的一种面向对象扩展)。
    • 利用 gcc 命令可同时编译并连接 CC++ 源程序。

编译器的工作过程

配置(configure)

  • 编译器在开始工作之前,需要知道当前的系统环境,比如标准库在哪里,软件的安装位置在哪里,需要安装那些组件等等.
  • 这是因为不同计算机的系统环境不一样,通过指定编译参数,编译器就可以灵活适应环境,编译出各种环境都能运行的机器码,这个确定编译参数的步骤,就叫做”配置(configure)”
  • 这些配置信息保存在一个配置文件中,约定俗成是一个叫做configure的脚本文件.通常它是由autoconf工具生成的.编译器通过运行这个脚本,获得编译参数

确定标准库和头文件的位置

  • 源码肯定会用到标准库函数(standard library)和头文件(header).它们可以放在系统的任意目录中,编译器实际上没有办法自动检测它们的位置,只有通过配置文件才能知道
  • 编译的第二步,就是从配置文件中知道标准库和头文件的位置.一般来说,配置文件会给出一个清单,列出几个具体的目录.等到编译时,编译器就按顺序到这几个目录中,寻找目标

确定依赖关系

  • 对于大型项目来说,源码之间往往存在依赖关系,编译器需要确定编译的先后顺序.假设A文件依赖于B文件,编译器应该保证做到:只有在B文件编译完成后,才开始编译A文件;当B文件发生变化时,A文件会被重新编译
  • 编译顺序保存在一个叫做makefile的文件中,里面列出哪个文件先编译,哪个文件后编译.而makefile文件由configure脚本运行生成,这就是为什么编译时configure必须首先运行的原因

头文件的预编译(precompilation)

  • 不同的源码文件,可能引用同一个头文件,编译的时候,头文件也必须一起编译
  • 为了节省时间,编译器会在编译源码之前,先编译头文件.这保证了头文件只需编译一次,不必每次用到的时候都需要重新编译
  • 不过,并不是头文件的所有内容都会被预编译,用来声明宏的#define命令,就不会被预编译

预处理(preprocessing)

  • 预编译完成后,编译器就开始替换掉源码中bash的头文件和宏,编译器在这一步还会移除注释

编译(Compilation)

  • 预处理之后,编译器就开始生成机器码.
  • 对于某些编译器来说,还存在一个中间步骤,会先把源码转为汇编码(assembly),然后再把汇编码转为机器码
  • 这种转码后的文件称为对象文件(object file)

连接(Linking)

  • 把外部函数的代码(通常是后缀名为.lib.a的文件),添加到可执行文件中,这就叫做连接(linking).
  • 这种通过拷贝,将外部函数库添加到可执行文件的方式,叫做静态连接(static linking)
  • make命令的作用,就是从第四步头文件预编译开始,一直到做完这一步.

安装(Installation)

  • 上一步的连接是在内存中进行的,即编译器在内存中生成了可执行文件.下一步,必须将可执行文件保存到用户事先指定的安装目录
  • 表面上,这一步就是将可执行文件(连带相关的数据文件)拷贝过去,但是实际上,这一步还必须完成创建目录,保存文件,设置权限等步骤,这整个的过程就称为”安装(Installation)”

操作系统连接

  • 可执行文件安装后,必须以某种方式通知操作系统,让其知道可以使用这个程序了
  • 这就要求在操作系统中,登记这个程序的元数据:文件名,文件描述,关联后缀名等等.linux系统中,这些信息通常保存在/usr/share/applications/目录下的.desktop文件中.另外在windows操作系统中,还需要在start启动菜单中,建立一个快捷方式
  • 这些事情就叫做”操作系统连接”.make install命令,就用来完成”安装”和”操作系统连接”这两步

生成安装包

  • 到这一步,源码编译的整个过程就基本完成了,但是事实上,如果只有源码可以交给用户,是不可行的,大部分用户要的是一个二进制的可执行程序,立刻就能运行.这就要求开发者,将上一步生成的可执行文件,做成可以分发的安装包
  • 所以,编译器还必须有生成安装包的功能,通常是将可执行文件(连带相关的数据文件),以某种目录结构,保存成压缩文件包,交给用户

动态连接(Dynamic linking)

  • 静态连接就是把外部函数库,拷贝到可执行文件中,这样做的好处是:适用范围较广,不用担心用户机器缺少某个库文件;缺点是:安装包会比较大,而且多个应用程序之间,无法共享文件.
  • 动态连接的做法正好相反,外部函数库不进入安装包,只在运行时动态引用.这样做的好处是:安装包比较小,多个应用程序可以共享文件;缺点是:用户必须事先安装好库文件,而且版本和安装位置都必须符合要求,否则就不能正常运行

ld – GNU linker(连接器)

  • 概述:
    • ld 合并一组目标文件(object)和库文件(archive),重定位数据部分,构建符号引用(symbolreference).
    • 一般说来,编译生成可执行文件的最后步骤就是调用ld

选项

  • -rpath directory
    • 增加一条对运行时(runtime)库的搜索路径. 这个选项用于连接ELF可执行文件和共享目标库
    • 所有-rpath选项的参数被合并,然后一起传递给运行时linker,运行时linker在运行的时候使用这些路径寻找共享目标库.
    • 如果连接ELF可执行文件时没有指定-rpath选项,linker就使用环境变量LD_RUN_PATH的内容,只要这个环境变量存在.
  • -Wl
    • 如果通过编译器驱动程序(例如 gcc)间接调用链接器,则所有链接器命令行选项都应以-Wl为前缀
    • 这很重要,因为否则编译器驱动程序可能会默默地删除链接器选项,从而导致链接错误
  • -s
    • 去掉输出文件中的全部符号信息
  • -g
    • 虚设项;用于兼容其他工具

g++

选项

  • -fPIC
    • 如果目标机器支持,则发出与位置无关的代码,适用于动态链接并避免对全局偏移表大小的任何限制
    • 与位置无关的代码需要特殊支持,因此仅适用于某些机器
    • 设置此标志时,宏“pic”和“PIC”被定义为 2
  • -Wno-unused-variable
    • 不显示未使用的变量告警
  • -Wno-unused-result
    • 不要警告标记了属性的函数的调用者是否使用它的返回值warn_unused_result(请参阅函数属性)。默认是-Wunused-结果
  • -Wno-deprecated-declarations
    • 不要警告使用属性标记为弃用的函数(请参阅函数属性),变量(请参阅变量属性)和类型(请参阅类型属性)deprecated
  • -DMACRO
    • 以字符串”1”定义 MACRO
  • -DMACRO=DEFN
    • 以字符串”DEFN”定义 MACRO 宏。
  • -DVERSION_MAJOR=${VERSION_MAJOR}
    • 以变量 VERSION_MAJOR 定义 VERSION_MAJOR
  • -funroll-loops
    • 循环展开,可以减少循环的次数,对程序的性能带了两方面的提高
    • 一是,减少了对循环没有直接贡献的计算,比如循环计数变量的计算,分支跳转指令的执行等
    • 二是,提供了进一步利用机器特性进行的优化的机会
  • -march=cpu-type
    • 优化选项。指定目标架构的名字,以及(可选的)一个或多个功能修饰符。
    • 此选项的格式为: -march = arch {+ [no] feature} *

编译优化

基本原理

  • 从运行时的依赖关系来看 :
    • 对性能有较大影响的组件有 kernel 和 glibc ,虽然这严格说来这不属于本文的话题,但是经过精心选择、精心配置、精心编译的内核与C库将对提高系统的运行速度起着基础性的作用
  • 从被编译的软件包来看 :
    • 每个软件包的 configure 脚本都提供了许多配置选项,其中有许多选项是与性能息息相关的。比如,对于 Apache-2.2.6 而言,你可以使用 –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 的前提下,没什么好说的,基本上新版本都能带来性能提升,同时比老版本对新硬件的支持更好,所以应当尽量选用新版本。不过追新也可能带来系统的不稳定,这 就要针对实际情况进行权衡了

CC和CXX

  • 这是 C 与 C++ 编译器命令。默认值一般是 “gcc” 与 “g++”

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 : 为了清除多余的溢出,在重载之后执行一个额外的载入消除步骤。
  • 参考链接:https://sites.google.com/site/polarisnotme/linux/gcc

LDFLAGS

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

  • -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以及动态链接器不兼容。