优化编译速度
C++ 的编译模型:
- 预处理:根据预处理指令(#include、#define、#if等)重新组装c++代码,经过预处理阶段,将产生一个没有注释、没有include、没有define、没有条件编译(#if/#else)等指令的.i文件
- 编译:经过预编译阶段输出的文件.i中包含字符串如main、if、while等,这些具体代表的意思还需要编译器经过词法分析、语法分析和语义分析,最后生成汇编代码.s文件
- 汇编:编译阶段得到的汇编代码并不能直接被计算机识别,汇编阶段通过汇编器把前面得到的汇编代码翻译成目标文件,生成.obj或.o目标文件,该文件中存放的就是与源代码等效的机器指令,每一个.cpp文件都会对应生成一个.obj或.o文件
- 链接:目标文件并不能直接执行,还需要经过链接过程,原因是:某个.cpp文件调用了另外.cpp文件中的函数或者常量等,它们是相互独立的(每个.cpp对应一个.obj文件),为了解决这类问题,必须要将调用者目标文件与被调用者的目标文件链接起来,最终得到可执行程序(.exe或.elf等)。大体分为静态链接和动态链接。
静态链接的特点:
- 静态库对函数库的链接是在编译时期完成的
- 程序在运行时与函数库再无瓜葛,移植方便
- 浪费空间和资源,因为和所需要函数相关的所有的库都会被打包进可执行文件
动态链接的特点:
- 动态库只有一份,可以实现进程之间的资源共享(因此动态库也称为共享库)
- 对库函数的链接推迟到程序运行时
- 程序升级变得简单
- 可以在链接载入时完全由程序员在程序代码中控制
动态链接的优势(静态链接的劣势):
- 静态链接浪费空间,所有用到的目标文件都会拷贝进来
- 静态链接库更新以后需要重新编译:库是被复制到可执行文件中去了,如果某个库更新了,则与它相关的所有可执行文件都需要重新编译
具体可参考:link
使用预编译头提高编译速度
在 C++ 的编译过程中,编译单元是源文件,在编译一个源文件之前,预处理器会把这个源文件中所有通过 #include
指令包含进来的头文件递归地展开,将所有直接或间接包含的头文件原封不动地插入进来,当这个过程结束之后,再开始编译。
这样编译的一个缺点是头文件会被重复编译。假如有一百个源文件都包含了Windows.h,那么这个头文件会在一百个源文件中展开,它里面的代码会被重复编译了一百次,尽管每次编译的结果都相同。对于具有成千上万个源文件的大型项目来说,重复编译是难以接受的,会浪费大量的编译时间。
为了解决这个问题,预编译头应运而生。顾名思义,预编译头就是预先把头文件编译好,在编译源文件的时候直接取用这些编译结果,避免对头文件重复编译。这项技术能大幅提高C++的编译速度。
Visual C++生成的扩展名为.pch的文件即是预编译头生成的结果。
通常来说,就是使用一个头文件,假设为 precompiled.h
,我们制定其为预编译头文件,可以将任意代码放到这个文件里,一般来说是将常用的头文件在这个文件中包含进来。但是要求这些代码必须是稳定的,在工程开发的过程中不会被经常改变。如果这些代码被修改,则需要重新编译生成预编译头文件,而生成预编译头文件是非常耗时间。
然后再新建一个 cpp 文件 precompiled.cpp
,这个文件里什么也不需要写,只需要将刚才的头文件 include 进来就好,因为我们只是需要它告诉编译器,要将这个东西编译一下。
#include "precompiled.h"
接下来只需要在 Visual Studio 中将预编译头选项设置为“使用”,并且将预编译头文件选择为刚才的 precompiled.h 就可以编译获得 .pch
文件了。
最后,需要在所有源文件中包含预编译头文件,并且该文件必须是第一个包含的。
每一个源文件只能使用一个预编译的头文件(.pch
),但是可以在一个项目中使用多个 .pch
文件。
注意事项
既然预编译头有这样的好处,那么是不是加入预编译的头文件越多越好呢?答案是否定的。上文已经提到,使用预编译头的时候必须在所有源文件中包含预编译头文件,由此造成的影响是,一旦其中的头文件发生了变化,不论这个变化有多细微,整个项目都要重新编译。把一个会被频繁修改的头文件包含到预编译头文件中是非常不明智的做法,因此,理想的选择是下列几乎不会修改的头文件:
- 操作系统API头文件,例如Windows.h。
- C/C++标准库头文件,例如string。
- 第三方库头文件,例如boost/filesystem.hpp。 另外一个要注意的是,C++的预编译头是不能用在C上的,反之亦然。也就是说,假如预编译头是通过.cpp源文件生成的,那么在.c源文件中使用了这个预编译头就会导致编译出错。
C++20 modules
Module(即模块)避免了传统头文件机制的诸多缺点,一个 Module 是一个独立的翻译单元,包含一个到多个 module interface file(即模块接口文件),包含 0 个到多个 module implementation file(即模块实现文件),使用 Import 关键字即可导入一个模块、使用这个模块暴露的方法。
有了 modules 以后, 我们可以模块化的处理。已经编译好的 modules 直接变成编译器的中间表示进行保存, 需要什么就取出什么, 这就非常地快速了。 比如你只是用了 cout 的函数, 那么编译器下次就不需要处理几万行, 直接找 cout 相关函数用就行了。