忧郁的大能猫
好奇的探索者,理性的思考者,踏实的行动者。
Table of Contents:
与其说Go的编译为什么快,不如先说说C++的编译为什么慢
C++编译慢的主要两个大头原因:
1、头文件的include方式 2、模板的编译
C++使用include方式引用头文件,会让需要编译的代码有乘数级的增加,例如当同一个头文件被同一个项目下的N个文件include时,编译器会将头文件引入到每一份代码中,所以同一个头文件会被编译N次(这在大多数时候都是不必要的);
C++使用的模板是为了支持泛型编程,在编写对不同类型的泛型函数时,可以提供很大的便利,但是这对于编译器来说,会增加非常多不必要的编译负担。
大部分后来的编程语言在引入文件的方式上,使用了import module来代替include 头文件的方式,import解决了重复编译的问题,当然Go也是使用的import方式;
C++至今(包括C++11)没有模块机制,不能像其他现代编程语言那样用import或using 来引入当前源文件用到的库(含其他package/module里的函数或类),而必须用include头文 件的方式来机械地将库的接口声明以文本替换的方式载入,再重新 parse 一遍。
这种方式有几个显著的缺点:
1. 重复解析:每次 #include 一个头文件时,编译器都会重新解析并处理这个头文件中的所有内容,即使它已经在其他地方被包含过。这会导致编译时间的增加,特别是在大型项目中。
2. 缺乏封装:#include 头文件实际上只是简单的文本替换,所有包含的代码都会被直接引入到当前编译单元中,缺乏封装性和独立性。头文件的任何更改都会导致相关文件重新编译,这也增加了维护的复杂性。
3. 命名冲突:由于所有包含的代码都在同一命名空间内,这也容易导致命名冲突。虽然可以使用命名空间(namespace)来部分解决这个问题,但这不是模块化的根本解决办法。
[[加快C++代码的编译速度方法]]。
好消息是,C++20 引入了模块(Modules)机制,旨在解决上述问题。C++ 模块允许将代码和接口封装在一个独立的模块中,这样可以减少重复解析,缩短编译时间,并提高代码的封装性。
C++20 引入的模块机制与传统的 #include 机制有着根本性的不同,模块机制的引入旨在提高编译效率、减少代码耦合和增强封装性。以下是 C++ 模块机制的编译原理及所涉及的主要步骤。
模块机制将代码分为接口单元(interface unit)和实现单元(implementation unit),并将它们组织在模块中。模块通过明确的导出和导入机制来控制符号的可见性和使用范围,从而实现更好的封装。
在传统的 #include 机制中,头文件的内容会在每个包含它的编译单元中被重新解析,而在模块机制中,模块接口会被编译为二进制格式的“模块接口文件”(通常称为 BMI,即 Binary Module Interface),其他编译单元可以直接引用这些 BMI 文件,从而避免了重复解析。
编译 C++ 模块时,通常分为以下几个步骤:
模块声明:在模块接口单元中,使用 export module module_name; 声明一个模块,并使用 export 关键字导出模块中的符号。
编译接口:编译器将接口单元中的代码编译为一个 BMI 文件。这个文件包含模块中导出符号的编译信息,如函数签名、类定义等。
// example.cpp
export module mymodule; // 声明模块
export void myFunction(); // 导出函数该代码在编译时,会生成一个与 mymodule 相关的 BMI 文件。
实现单元:模块的实现单元使用 module module_name; 来引用模块,可以选择不导出符号(这些符号只在模块内部可见)。
编译实现单元:实现单元中包含的具体实现代码会被编译为目标文件(.o 或 .obj),但它不会生成额外的 BMI 文件。
module mymodule; // 引用模块
void myFunction() {
// 实现函数
}导入模块:在其他编译单元中使用 import module_name; 导入模块,编译器会查找并加载对应的 BMI 文件,而不需要重新解析模块接口的源代码。
import mymodule; // 导入模块
int main() {
myFunction(); // 使用模块中的函数
}编译用户代码:编译器使用已编译好的 BMI 文件来解析导入的模块,并生成最终的目标文件。
大多数现代 C++ 编译器(如 GCC、Clang 和 MSVC)在支持 C++20 模块时,都会实现模块的缓存机制。编译器会将已编译好的 BMI 文件缓存起来,以避免在多次编译中重复生成这些文件,这可以显著提升编译速度。