要添加到预编译头中的内容

What to add to precompiled headers

本文关键字:编译 添加      更新时间:2023-10-16

我是预编译头文件的新手,只是想知道该包含什么。我们的项目有大约200个源文件。

那么,我真的包括了每一个第三方图书馆吗?

如果我在三个源文件中使用映射,我会添加它吗?如果我使用它一个,我会添加它吗?我需要删除旧的直接include还是ifdef和pragmaonce指令仍然有效?

有没有你不会添加的第三方库?

那么预编译的头不是很大吗?

就像在中一样,即使是以预编译的形式,也会突然将所有这些头都包含在各处,这难道不是一种开销吗?

编辑:

我在clang上找到了一些信息:

在以下情况下,预编译头实现可提高性能:

  • 加载PCH文件比重新解析存储在PCH文件中的头束要快得多。因此,预编译头设计试图将读取PCH文件的成本降至最低。理想情况下,此成本不应随预编译头文件的大小而变化
  • 最初生成PCH文件的成本并不是很大,因为它从一开始就不需要解析绑定的头,所以它抵消了每个源文件的性能改进。这在多核系统上尤其重要,因为当所有编译都要求PCH文件是最新的时,PCH文件生成会序列化构建

Clang的预编译头文件采用磁盘上紧凑的表示形式设计,最大限度地减少了PCH创建时间和初始加载PCH文件所需的时间。PCH文件本身包含Clang的抽象语法树和支持数据结构的序列化表示,使用与LLVM的位代码文件格式相同的压缩位流存储

Clang的预编译头文件是从磁盘"惰性地"加载的。当最初加载PCH文件时,Clang只从PCH文件中读取少量数据,以确定某些重要数据结构的存储位置。在该初始加载中读取的数据量与PCH文件的大小无关,因此较大的PCH文件不会导致较长的PCH加载时间。PCH文件中的实际头数据——宏、函数、变量、类型等——只有在从用户代码中引用时才会加载,此时只有该实体(以及它所依赖的实体)才会从PCH文件反序列化。使用这种方法,将预编译头用于翻译单元的成本与头实际使用的代码量成比例,而不是与头本身的大小成比例

对我来说,这似乎表明至少叮当作响:

  • 注意使预编译头文件的加载时间与大小无关
  • 预编译头的使用时间与预编译头大小无关,并且与使用的信息量成比例
  • 与目前给出的答案相反,这似乎表明,即使只包含一次外部文件(如<map>),也值得将其包含在预编译的头文件中(仍将加快该源文件的重新编译)

必须有某种地图来映射所有信息。这张地图可能会变大,但也许这并不那么重要?但不确定我是否做对了,或者它是否适用于所有编译器。。。

这个问题没有100%的确切答案,因为它取决于您的项目。最好的办法是自己尝试一下,看看会发生什么。

然而,

"那么,我真的包括所有第三方库吗?">

不,基本上你包括了标题,它是:

  • 经常被您的来源使用。然而,"经常"并没有完全定义,但假设它被超过10%的源文件使用。(不过,我随机选择了这个数字。也许它应该更大一些。)
  • 大部分时间都没有更改(因为更改单个标头意味着您需要重新编译所有源代码)。第三方库预计不会更改,因此它们是最佳的候选库,但如果您确信它们很少更改或在特殊情况下更改,您也可以使用自己项目中的头文件

但不要"只是"包含库的所有头。包括您正在使用的。

"如果我在三个源文件中使用映射,我要添加它吗?">

请参见上文。对此没有明确的答案,但我个人认为三个源文件太少了。

"如果我使用它一个,我会添加它吗?">

(我理解这个问题,因为它将是"如果我在单个源中使用头文件并将其添加到预编译头中会怎样?">)

没有什么会破坏你的应用程序。但它将使:

  • 编译预编译头的时间更长
  • 预编译的头文件更大
  • 源文件的编译速度较慢

在单个标头的情况下,如果标头大小为平均值,则完全无关紧要。然而,如果您添加了数百个这样的头,就会减慢整个编译的速度。

"我需要删除旧的直接include吗?还是ifdef和pragmaonce指令仍然有效?">

你可能可以做这样的事情,但我强烈建议不要这样做。然而,你不需要这样做。

您可以想象,预编译的头只是包含在所有源代码开头的头中。示例:

预编译.h

#include <iostream>
#include <string>

MyClass.cpp

#include "MyClass.h"
MyClass::MyClass()
{
// etc.

现在假设您启用了预编译头。对于源文件,它就像你要写的一样:

#include "precompiled.h"
#include "MyClass.h"
MyClass::MyClass()
{
// etc. 

你能正常地那样做吗?是的,你可以!预编译头的作用是这样的(但它更快),这意味着:

  • 是的,宏被保留。预编译头中定义的内容都在所有源文件中定义,这意味着:
    • 警卫工作正常
    • 如果有一个检测操作系统的库,那么所有宏仍然是定义的并且可用
    • 如果定义了其他宏(例如MIN(尽管现在建议使用std::MIN)),您仍然可以正常使用它
  • 我不知道pragma,但我相信它也能正常工作

关于删除源代码中的includes:如上所述,我强烈反对。原因很简单:如果将来需要关闭预编译头,该怎么办?(事实上,我个人会不时关闭预编译头,看看我的代码是否仍然可以编译。我个人的原因是,如果你发布代码,一些用户将不会使用你的项目/make文件,但他们会创建自己的项目(例如,如果他们使用不同的IDE,如code::Blocks或QtCreator),所以我试图以这样的方式创建我的项目,即源文件,配置正确的包含路径,链接正确的库,它应该编译。)

"有没有你不会添加的第三方库?">

我想不出任何。。。

另一方面,我可以想到一些IMHO最好添加的内容(如果你使用它们的话),例如boost。大多数情况下,它使用模板——根据我的个人经验,模板是编译速度最慢的,因为你不仅需要包含onlu声明,还需要包含定义。IMHO,这是C++模板最大的弱点。

"预编译的头不是变得巨大了吗?">

他们可以。这就是为什么您需要找到头文件的最佳子集(而不是盲目地包含所有内容)以获得最佳结果。我的大约50MB大,但仍然大大加快了编译速度。(整整几分钟,因为我经常使用模板。)

"就像在中一样,即使是以预编译的形式,也会突然在所有地方包含所有这些标头,这难道不是一种开销吗?">

如果使用预编译头,则需要准备一组头,并将它们包含在所有源文件中。这意味着,从单个源文件的角度来看,您包含了一些源文件不需要的头那是开销。但是,包含预编译的头要快得多,所以如果包含一些不需要的头,它会更快。然而,当您超过某个限制时(假设对于超过90%的源,超过90%的包含头是不需要的),当使用预编译头时,开始减慢编译速度。这就是为什么您需要包含大多数使用的头文件,并避免包含仅包含在少数源文件中(或根本不使用)的头文件。

通常,使用预编译的头文件会增加磁盘上所需的空间(在这种情况下,这绝对不重要)和RAM中所需的时间(同样,在这种情况中,这并不重要)。这是"以内存为代价提高速度"的完美例子。


最后一条建议很简单:自己试试。检查当您添加标头时会发生什么,当您觉得编译很慢时,检查您是否包含了大多数未使用的内容。

正如您所知,编译C/C++源代码是一项耗时的任务,其中一个原因是编译器需要编译您直接或间接包含到源代码中的每一块代码,在大多数情况下,这是一种冗余,因为所包含的大多数文件都是库,不会随着时间的推移而更改。为了缓解这个问题,引入了预编译头的概念。通过预编译头,可以告诉编译器一组包含文件不太可能随着时间的推移而改变,通过这种方式,编译器可以通过一次编译指定的文件并保存结果来优化编译过程。然后,每当编译器需要编译项目时,它都会跳过指定源的编译,并重用这些保存的编译文件。

因此,在预编译头中包含不频繁更改的文件是一个好主意。当然,您可以添加经常更改的代码,但这将忽略使用预编译头的全部意义。

顺便说一句,不要担心预编译的头文件变得庞大。这个概念主要是为了减少大型项目的编译时间,在这些项目中通常有过多的第三方库。在这些情况下,这些文件通常会变得巨大。

另请参阅维基百科关于预编译头的条目。