背景
最近服务器编译速度越来越慢,影响开发效率,着手改进。当前环境是:CentOS Linux release 7.2.1511,g++ 4.8.5。
方法研究
ccache
ccache是一个编译缓存工具,其原理是将cpp的编译结果保存在文件缓存中,以后编译时若对应文件无变动可直接从缓存中获取编译结果。
由于我们直接用makefile管理项目,会保存.o文件,头文件变化时make会重新生成.o文件,ccache的命中率很低,基本没什么用。
distcc
distcc是一个分布式编译工具。distcc可以在普通模式或泵模式下使用。简单来讲,这两个模式的核心区别在于如何处理预处理过的源代码。普通模式会传递预处理器展开过的源代码和编译选项,由客户机负责编译,而泵模式会把预处理和编译工作全部分发到客户机,通常会更快。我发现预处理其实很快,虽然预处理后的文件会比源文件大很多,至少网络传输会快一些,但我们是局域网,这也无关紧要,就用普通模式了。
先从distcc官网下载3.0版本,因为高版本的依赖环境更高,我们也不需要那么全面复杂的功能,用一个比较老的版本就行。解压后根据INSTALL文件安装,下面是具体过程:
./autogen.sh
./configure
make
make rpm
“make”的时候由于它开启了-Werror选项,会把警告当成错误,搜一下所有出现-Werror的地方,将其删掉。
“make rpm”的时候如果报“error: Installed (but unpackaged) file(s) found”,找到 /usr/lib/rpm/macros 中的“%__check_files /usr/lib/rpm/check-files %{buildroot}”,将其注释掉后重试一次。
最后会在packaging目录下生成distcc-3.0-1.x86_64.rpm,distcc-server-3.0-1.x86_64.rpm。
由于我们每台开发机都是平等的,所以将每台机器都分别作为server端和client端,将上述两个rpm传到每台开发机上,用rpm -i
命令安装。
安装distcc-server-3.0-1.x86_64.rpm时会报错,因为配置文件还没修改,不用管。
在每台机器上做如下配置:
/etc/distcc/clients.allow
10.68.12.0/24
# 表示服务端接受10.68.12.0网段的客户端请求
/etc/distcc/hosts
--randomize 10.68.12.50/16 10.68.12.51/16 localhost/16
# 表示客户端会将请求发至10.68.12.50,10.68.12.51和localhost,每个服务端最多启用16个进程,
# --randomize表示每个服务端都是平等的。
然后执行:systemctl restart distcc.service
最后将makefile中的g++命令改为distcc g++即可。
完整编译的时间并没有加快很多,但多人同时进行编译时,机器没那么卡了,还是有些用处。
并行编译
服务器包括多个可执行程序,现在是依次编译,前一个链接完后一个才开始编译,考虑让它们同时编译。但global下的几个源文件又是编译几个可执行程序所需要的,同时编译会不会有冲突?
合并源文件
把所有的cpp包含到一个cpp中,然后只编译这一个cpp,这种方法被称为Unity Build。
这种方式下,所有共享头文件只需要解析一次。许多C ++项目都有大量的头文件,大多数CPP文件都包含这些头文件,这些文件的冗余解析是编译的主要成本,特别是如果你有很多短源文件。冗余头解析的减少意味着内联函数的冗余代码减少,因此目标文件的总大小更小,这使得链接更快。
但如果这样做,就没法利用多核,只能单线程编译链接,需要平衡源文件合并的数量,而且如有编译错误,可能得等很久编译器才会报错。
使用ld.gold
据说ld.gold比默认的链接器快很多,使用选项-fuse-ld=gold启用ld.gold,试了下链接时间从65秒变为25秒,确实快了不少。
减小预编译文件大小
由于很多源文件引用了一些很大的头文件,导致预编译后的源文件变得很大,根据参考1的说法,“cpp文件的编译耗时,和其预编译展开文件大小呈正相关(绝大部分情况下)”,所以考虑减少引用头文件的规模。
一、分析不合理头文件包含
Include-what-you-use工具是Google推出,基于Clang的C/C++工程冗余头文件检查工具。该工具安装起来比较麻烦,带来的速度提升也不高,暂不考虑。
二、外部模板
由于模板被使用时才会实例化这一特性,相同的实例可以出现在多个文件对象中。编译器要对每一处模板进行实例化,链接器还要移除重复的实例化代码。C++ 11新标准中可以通过外部模板来避免。该方法需要手动修改代码,比较麻烦,带来的速度提升也不高,暂不考虑。
三、使用前置类型声明,而不引用相关头文件
找到被引用数量多的头文件,将其中的类做前置声明,才更有效用。
四、拆分大头文件,用到哪个就包含哪个
该方法可有效减少预编译文件大小,并且对增量编译也有积极作用。
五、预编译一些公共的且不经常变化的头文件
我们的公共头文件一般变化比较频繁,所以暂不考虑该方法。
我的思路
先用一个工具,来统计哪些头文件被引用的最多,然后尝试将其拆分或做前置类型声明。
增量编译
make本身有一定缓存功能,当目标文件已编译且依赖无变化时,若源文件时间戳无变化也不会再次编译,但我们很多头文件是脚本自动生成的,可能内容不会变化,但时间戳会更新,也会触发重新编译。
考虑将部分头文件的摘要写入另一个文件,让源文件依赖这个摘要文件,头文件内容有变化时更新该摘要文件,会触发源文件的重新编译,而头文件内容没变时,摘要文件不变,不会重新编译。
具体行动
1、使用distcc
2、使用ld.gold
3、写头文件分析工具
4、将hot头文件做拆分或前置类型声明
5、实现增量编译