【转载】CMake PreCompiled Header: 基于CMake的预编译头文件

传统的编译方式中存在着什么问题

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的基础上完成编译提速.

预编译头文件今天在改一个很大的程序,慢慢看,慢慢改。突然发现一个.c文件,里面什么也没有,就几个头文件,我一看,我靠,这不是把简单的问题搞复杂了吗,随手删掉那个c文件。结果不能编译了,我靠:fatal error C1083: Cannot open precompiled header file: \'Debug/v13_3.pch\':No such file or directory怎么rebuild all都不行。上网查了一下,才搞懂了:----------------总结------如果工程很大,头文件很多,而有几个头文件又是经常要用的,那么1。把这些头文件全部写到一个头文件里面去,比如写到preh.h2。写一个preh.c,里面只一句话:#include "preh.h"3。对于preh.c,在project setting里面设置creat precompiled headers,对于其他.c文件,设置use precompiled header file//哈哈我试了一下,效果很明显,不用precompiled header,编译一次我可以去上个厕所,用precompiled header,编译的时候,我可以站起来伸个懒腰,活动活动就差不多啦---------转载的文章----------预编译头的概念:所谓的预编译头就是把一个工程中的那一部分代码,预先编译好放在一个文件里(通常是以.pch为扩展名的),这个文件就称为预编译头文件这些预先编译好的代码可以是任何的C/C++代码--------甚至是inline的函数,但是必须是稳定的,在工程开发的过程中不会被经常改变。如果这些代码被修改,则需要重新编译生成预编译头文件。注意生成预编译头文件是很耗时间的。同时你得注意预编译头文件通常很大,通常有6-7M大。注意及时清理那些没有用的预编译头文件。也许你会问:现在的编译器都有Time stamp的功能,编译器在编译整个工程的时候,它只会编译那些经过修改的文件,而不会去编译那些从上次编译过,到现在没有被修改过的文件。那么为什么还要预编译头文件呢?答案在这里,我们知道编译器是以文件为单位编译的,一个文件经过修改后,会重新编译整个文件,当然在这个文件里包含的所有头文件中的东西(.eg Macro, Preprocesser )都要重新处理一遍。VC的预编译头文件保存的正是这部分信息。以避免每次都要重新处理这些头文件预编译头的作用:根据上文介绍,预编译头文件的作用当然就是提高便宜速度了,有了它你没有必要每次都编译那些不需要经常改变的代码。编译性能当然就提高了。预编译头的使用:要使用预编译头,我们必须指定一个头文件,这个头文件包含我们不会经常改变的代码和其他的头文件,然后我们用这个头文件来生成一个预编译头文件(.pch文件)想必大家都知道 StdAfx.h这个文件。很多人都认为这是VC提供的一个“系统级别”的,编译器带的一个头文件。其实不是的,这个文件可以是任何名字的。我们来考察一个典型的由AppWizard生成的MFC Dialog Based 程序的预编译头文件。(因为AppWizard会为我们指定好如何使用预编译头文件,默认的是StdAfx.h,这是VC起的名字)。我们会发现这个头文件里包含了以下的头文件:#include // MFC core and standard components#include // MFC extensions#include // MFC Automation classes#include // MFC support for Internet Explorer 4Common Controls#include 这些正是使用MFC的必须包含的头文件,当然我们不太可能在我们的工程中修改这些头文件的,所以说他们是稳定的。那么我们如何指定它来生成预编译头文件。我们知道一个头文件是不能编译的。所以我们还需要一个cpp文件来生成.pch 文件。这个文件默认的就是StdAfx.cpp。在这个文件里只有一句代码就是:#include “Stdafx.h”。原因是理所当然的,我们仅仅是要它能够编译而已?D?D?D也就是说,要的只是它的.cpp的扩展名。我们可以用/Yc编译开关来指定StdAfx.cpp来生成一个.pch文件,通过/Fp编译开关来指定生成的pch文件的名字。打开project ->Setting->C/C++ 对话框。把Category指向Precompiled Header。在左边的树形视图里选择整个工程 Project Options(右下角的那个白的地方)可以看到 /Fp “debug/PCH.pch”,这就是指定生成的.pch文件的名字,默认的通常是 .pch(我的示例工程名就是PCH)。然后,在左边的树形视图里选择StdAfx.cpp.//这时只能选一个cpp文件!这时原来的Project Option变成了 Source File Option(原来是工程,现在是一个文件,当然变了)。在这里我们可以看到 /Yc开关,/Yc的作用就是指定这个文件来创建一个Pch文件。/Yc后面的文件名是那个包含了稳定代码的头文件,一个工程里只能有一个文件的可以有YC开关。VC就根据这个选项把 StdAfx.cpp编译成一个Obj文件和一个PCH文件。然后我们再选择一个其它的文件来看看,//其他cpp文件在这里,Precomplier 选择了 Use ???一项,头文件是我们指定创建PCH 文件的stdafx.h文件。事实上,这里是使用工程里的设置,(如图1)/Yu”stdafx.h”。这样,我们就设置好了预编译头文件。也就是说,我们可以使用预编译头功能了。以下是注意事项:1):如果使用了/Yu,就是说使用了预编译,我们在每个.cpp文件的最开头,我强调一遍是最开头,包含 你指定产生pch文件的.h文件(默认是stdafx.h)不然就会有问题。如果你没有包含这个文件,就告诉你Unexpected file end. 如果你不是在最开头包含的,你自己试以下就知道了,绝对有很惊人的效果?..fatal error C1010: unexpected end of file while looking for precompiledheader directiveGenerating Code...2)如果你把pch文件不小心丢了,编译的时候就会产生很多的不正常的行为。根据以上的分析,你只要让编译器生成一个pch文件。也就是说把 stdafx.cpp(即指定/Yc的那个cpp文件)从新编译一遍。当然你可以傻傻的 Rebuild All。简单一点就是选择那个cpp文件,按一下Ctrl + F7就可以了。不然可是很浪费时间的哦。//呵呵,如果你居然耐着性子看到了这里,那么再回到帖子最开始看看我的总结吧!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值