2.2 C++编译特点(1)每个源文件独立编译
C/C++的编译系统和其他高级语言存在很大的差异,其他高级语言中,编译单元是整个Module,即Module下所有源码,会在同一个编译任务中执行。而在C/C++中,编译单元是以文件为单位。每个.c/.cc/.cxx/.cpp源文件是一个独立的编译单元,导致编译优化时只能基于本文件内容进行优化,很难跨编译单元提供代码优化。
(2)每个编译单元,都需要独立解析所有包含的头文件
如果N个源文件引用到了同一个头文件,则这个头文件需要解析N次(对于Thrift文件或者Boost头文件这类动辄几千上万行的头文件来说,简直就是“鬼故事”)。
如果头文件中有模板(STL/Boost),则该模板在每个cpp文件中使用时都会做一次实例化,N个源文件中的std::vector会实例化N次。
(3)模板函数实例化
在C++ 98语言标准中,对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。显然编译器遇到一个模板定义时,每次都去进行重复的实例化工作,进行重复的编译工作。此时,如果能够让编译器避免此类重复的实例化工作,那么可以大大提高编译器的工作效率。在C++ 0x标准中一个新的语言特性 -- 外部模板的引入解决了这个问题。
在C++ 98中,已经有一个叫做显式实例化(Explicit Instantiation)的语言特性,它的目的是指示编译器立即进行模板实例化操作(即强制实例化)。而外部模板语法就是在显式实例化指令的语法基础上进行修改得到的,通过在显式实例化指令前添加前缀extern,从而得到外部模板的语法。
① 显式实例化语法:template class vector。 ② 外部模板语法:extern template class vector。
一旦在一个编译单元中使用了外部模板声明,那么编译器在编译该编译单元时,会跳过与该外部模板声明匹配的模板实例化。
(4)虚函数
编译器处理虚函数的方法是:给每个对象添加一个指针,存放了指向虚函数表的地址,虚函数表存储了该类(包括继承自基类)的虚函数地址。如果派生类重写了虚函数的新定义,该虚函数表将保存新函数的地址,如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址将被添加到虚函数表中。
调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。
使用虚函数后的变化:
① 对象将增加一个存储地址的空间(32位系统为4字节,64位为8字节)。 ② 每个类编译器都创建一个虚函数地址表。 ③ 对每个函数调用都需要增加在表中查找地址的操作。
(5)编译优化
GCC提供了为了满足用户不同程度的的优化需要,提供了近百种优化选项,用来对编译时间,目标文件长度,执行效率这个三维模型进行不同的取舍和平衡。优化的方法不一而足,总体上将有以下几类:
① 精简操作指令。 ② 尽量满足CPU的流水操作。 ③ 通过对程序行为地猜测,重新调整代码的执行顺序。 ④ 充分使用寄存器。 ⑤ 对简单的调用进行展开等等。
如果全部了解这些编译选项,对代码针对性的优化还是一项复杂的工作,幸运的是GCC提供了从O0-O3以及Os这几种不同的优化级别供大家选择,在这些选项中,包含了大部分有效的编译优化选项,并且可以在这个基础上,对某些选项进行屏蔽或添加,从而大大降低了使用的难度。
3.4 分析工具建设通过上面的工具分析能拿到几个编译数据:
① 头文件依赖关系及个数。 ② 预编译展开大小及内容。 ③ 各个文件编译耗时。 ④ 整体链接耗时。 ⑤ 可以计算出编译并行度。
通过这几个数据的输入我们考虑可以做个自动化分析工具,找出优化点以及界面化展示。基于这个目的,我们建设了全流程自动化分析工具,能够自动分析耗时共性问题以及TopN耗时文件。分析工具处理流程如下图所示:
(1) 整体统计分析效果
具体字段说明:
① cost_time 编译耗时,单位是秒。 ② file_compile_size,编译中间文件大小,单位是M。 ③ file_name,文件名称。 ④ include_h_nums,引入头文件个数,单位是个。 ⑤ top_h_files_info, 引入最多的TopN头文件。
(2)Top10 编译耗时文件统计
用来展示统计编译耗时最久的TopN文件,N可以自定义指定。