传统的编译方式中存在着什么问题
C++的编译过程可以大致分为 预处理
,编译
,汇编
,链接
,至少就现在(2021年九月)来讲,主流编译器的前三步的编译单元都是独立的单个文件.
这意味着,如果有复数个文件中包含着相同的头文件,比如<algorithm>
,这些头文件在每个文件中,都会被预处理器导入,被词法分析,语法分析,语义分析走一遍流程.
预处理器
受限于模板的表达能力,标准模板库(STL)中使用了很多Header-Only
的实现,这导致其实在预处理阶段,虽然只是include了几个头文件,但是其实头文件会层层include,最后include出来
比如下文的test.cpp
#include <iostream>
#include <cstdint>
int main(int argc,char** argv){
return argc;
}
经过gcc -E ./test.cpp > test.cpp.E
之后会产出3万行左右,尽管实际上只有几行代码有效.
词法分析与语法分析
经过了预处理器的导入后,这足足三万行代码都会被经过完整的词法分析语法分析语义分析,这导致了多个文件,纵使其实很大部分都是预处理器导入的相同代码,但是仍然需要对这些代码进行重复的处理.
如何解决传统方式导致的耗时长问题?
这些问题可以总结为,由于C++的编译模型强调前三步的编译单元都是单个文件,所以这些被引入到单个文件中的重复内容无法被利用.其实在不同程度下,可以有不同的优化级别.
避免使用多个编译单元
既然无法跨编译单元,那就将类似的文件整合到一起罢. 将一些关系紧密的cpp文件include到一个文件中,再用这个文件编译,就解决了问题.
PS: 这种情况下,编译由原来的多线程变成了单线程,因此需要经过测试.
预处理级别
首先,预处理这个级别每个文件中都会做,那么可以通过一些手段,令所有文件都引用一个"编译期"生成的文件,这个文件具有所有其他文件的include,并且在所有cpp文件编译之前编译,以此来节约其他文件预编译的时间.
PS: 这个实现很类似Flex和Bison在写编译器时候的手法
编译级别
既然 “编译期"生成的文件可以进行预处理,可不可以再进一步,对它进行一下词法分析语法分析语义分析,产生一个二进制文件来节约时间? 实际上,这个功能在编译器中有实现,跨平台的CMake也在3.16.6中添加了支持.
CMake
由于CMake提供了跨平台支持,现在就不需要用传统的土法来写预编译头了,可以使用Modern CMake来实现
基础实现
最基础的实现莫过于
target_precompile_headers(${TARGET_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/header.hpp
<cstdint>
)
这一类声明,给${TARGET_NAME}引入指定的库文件与头文件以预编译头文件.
PS:值得注意的是,虽然这里附加上了预编译头文件,但是实现其实是在编译期先根据指令来产生预编译头文件,然后默认插入到target中所有的源文件的最前方,因此,编辑器无法识别这个信息,所以还需要传统的include来引导编辑器来做词法分析方便渲染颜色. 不需要担心的是,这个过程中宏会得到保留,所以不会一个文件引入多次.
但是此时每一个target实际上都会存有一份预编译头文件,一旦target多起来,预编译头文件占有的体积会超级大(基本预编译头文件的尺寸在百兆级别).
复用预编译头文件
由于预编译头文件的体积问题,target之间复用预编译头文件就是必要的,但是值得注意的是,由于c++中存在外部注入的宏这样的存在,因此只有在 “所有编译选项 && 宏"都一致的情况下,复用头文件才是合理的.
复用头文件建议使用"三级分层"机制,
level1, 预编译头文件的总头文件, 引入一个INTERFACE target
,只添加target_precompile_headers
level2, 预编译头文件每一个编译特性都应该有一个level2 target, 这一层同样是一个INTERFACE target
,本层用来添加所有的编译选项, 同时以link level1 target的方式获取预编译头文件,
level3, 所有真实使用的target,选择性的复用某一个level2的target,获取符合自己编译特性的预编译头文件.
此处level3使用如下方式
target_precompile_headers(${level3}_${target_name} REUSE_FROM
${level2}_${target_name}
)
使用REUSE_FROM
关键字来复用预编译头文件
结论
预编译头文件以一个十分巧妙的方式,节约了在头文件中预处理,词法解析语法解析语义分析的重复步骤,以中间产物空间换时间,能够在不修改源文件,只修改CMake的基础上完成编译提速.