C++服务编译耗时优化原理及实践

大型C++工程项目,都会面临编译耗时较长的问题 。不管是开发调试迭代、准入测试,亦或是持续集成阶段,编译行为无处不在,降低编译时间对提高研发效率来说具有非常重要意义 。

C++服务编译耗时优化原理及实践

文章插图
 
一、背景美团搜索与NLP部为公司提供基础的搜索平台服务,出于性能的考虑,底层的基础服务通过C++语言实现,其中我们负责的深度查询理解服务(Deep Query Understanding,下文简称DQU)也面临着编译耗时较长这个问题,整个服务代码在优化前编译时间需要二十分钟左右(32核机器并行编译),已经影响到了团队开发迭代的效率 。
在这样的背景下,我们针对DQU服务的编译问题进行了专项优化 。在这个过程中,我们也积累了一些优化的知识和经验,在这里分享给大家 。
二、编译原理及分析2.1 编译原理介绍
为了更好地理解编译优化方案,在介绍优化方案之前,我们先简单介绍一下编译原理,通常我们在进行C++开发时,编译的过程主要包含下面四个步骤:
C++服务编译耗时优化原理及实践

文章插图
 
预处理器:宏定义替换,头文件展开,条件编译展开,删除注释 。
  • gcc -E选项可以得到预处理后的结果,扩展名为.i 或 .ii 。
  • C/C++预处理不做任何语法检查,不仅是因为它不具备语法检查功能,也因为预处理命令不属于C/C++语句(这也是定义宏时不要加分号的原因),语法检查是编译器要做的事情 。
  • 预处理之后,得到的仅仅是真正的源代码 。
编译器:生成汇编代码,得到汇编语言程序(把高级语言翻译为机器语言),该种语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令 。
  • gcc -S选项可以得到编译后的汇编代码文件,扩展名为.s 。
  • 汇编语言为不同高级语言的不同编译器提供了通用的输出语言 。
汇编器:生成目标文件 。
  • gcc -c选项可以得到汇编后的结果文件,扩展名为.o 。
  • .o文件,是按照的二进制编码方式生成的文件 。
链接器:生成可执行文件或库文件 。
  • 静态库:指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了,其后缀名一般为“.a” 。
  • 动态库:在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可执行文件比较小,动态库一般后缀名为“.so” 。
  • 可执行文件:将所有的二进制文件链接起来融合成一个可执行程序,不管这些文件是目标二进制文件还是库二进制文件 。
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 。


推荐阅读