不必要的包含文件是否会增加负担?

27

我看到了一些关于如何检测C++项目中不必要的#include文件的问题。这个问题经常让我感到好奇,但我从来没有找到一个令人满意的答案。

如果有一些被包含在C++项目中但未被使用的头文件,那么它们是否是一种负担?我知道这意味着在编译之前,所有头文件的内容都会被复制到被包含的源文件中,这将导致大量不必要的编译。

这种开销会对编译后的目标文件和二进制文件产生多大的影响?

编译器难道不能进行一些优化以确保这种开销不会转移到生成的目标文件和二进制文件中吗?

考虑到我可能对编译器优化一无所知,我仍然想问这个问题,以防有答案。

作为一个使用各种c++库进行工作的程序员,我应该遵循什么样的编程实践来避免这种开销?除了熟悉每个库的工作方式外,还有其他方法吗?


这取决于包含文件中的内容。结构声明不会产生任何运行时开销,而外部变量声明则会产生开销。 - Barmar
4
声明一个变量(即告诉编译器该变量的名称已在其他地方定义)真的会有运行时成本吗?为什么? - user395760
1
@Barmar 这取决于你包含了哪些依赖项(有多远的上游)。通过引入一个远处的依赖项(取决于你的构建/重建设置和你所做的更改),你可以将一个10分钟的项目重建变成1.5小时的重建。 - Cory Kramer
3
当变量在另一个翻译单元中被定义时,它会在那里进行初始化,而不是在你声明其存在的地方进行初始化。 - user395760
1
我认为你应该去掉C标签。C和C++在这方面有很大的不同。正如某些评论中所说,C++头文件甚至倾向于定义一些未使用但仍可能不被优化的符号。而且,对于C++来说,编译时间比C更重要。因此,两种语言的答案可能会非常不同。 - Jens Gustedt
显示剩余5条评论
4个回答

26
几乎所有头文件的性能甚至二进制文件的内容都不受影响。声明根本不生成任何代码,inline/static/匿名名称空间定义如果未使用则会被优化掉,而且没有头文件应该包含外部可见的定义(如果头文件被多个翻译单元包含,则会破坏该定义)。
正如@T.C.所指出的,带有非平凡构造函数的内部可见静态对象是例外情况。例如,iostream就是这样做的。程序必须表现得好像调用了构造函数,编译器通常没有足够的信息来优化构造函数。
但是,它确实会影响编译需要的时间以及更改头文件时需要重新编译的文件数量。对于大型项目,这足以激励人们关注不必要的包含。

有一些例外情况。头文件中的函数定义在每个包含它们的二进制文件中都会重复出现,较大的二进制文件加载时间更长,并且重复的函数会干扰指令缓存,导致微小的性能损失。 - Mooing Duck
@MooingDuck 链接器通常可以将它们排除掉,不是吗? - T.C.
@T.C.:它会在将多个目标文件链接成一个二进制文件时删除函数的重复部分。但是,它无法删除在多个 DLL 或 EXE 中使用的函数。 - Mooing Duck
@MooingDuck 我认为我在回答中已经解决了这个问题?这里的假设是包含是不必要的,即函数未被使用,因此可以由编译器剥离。你是在谈论外部可见定义吗?它们与ODR(以及C中的等效规则)冲突,因此不能有重复的定义。 - user395760
2
头文件可以使用内部链接定义对象,这样你就需要为它们的初始化付出代价。(例如,包括<iostream>时,必须表现得好像它定义了一个具有静态存储期的ios_base::Init对象。) - T.C.
显示剩余3条评论

6
除了显然更长的编译时间,可能还有其他问题。我认为最重要的问题是对外部库的依赖。您不希望您的程序依赖于比必要更多的库。
您还需要在每个要构建程序的系统上安装这些库。这可能会成为一场噩梦,特别是当下一个程序员需要安装某个数据库客户端库时,尽管该程序从未使用过数据库。
此外,特别是库头文件往往倾向于定义宏。有时这些宏具有非常通用的名称,这将破坏您的代码或与您实际需要的其他库头文件不兼容。

如果你从未使用外部库中的任何内容,那么你是否真的对它有一个“依赖”呢?例如,如果你只是#include它但从未引用其任何内容。 - Mr. Llama
@Mr.Llama,你可能正在考虑链接器会丢弃任何东西,甚至不强制你与实际库进行链接。但是你需要安装库以便找到头文件,否则你的代码将无法编译。从法律上讲,这也可能已经算作依赖项的许可证。因此,即使只是为了构建项目,你已经依赖于该库。 - Daniel Frey

3
当然,任何#include都是一种负担。编译器需要解析该文件。
因此请避免使用它们。尽可能使用前向声明。
这将加速编译。请参阅Scott Myers的相关书籍。

7
我猜他主要关心运行时开销。 - Barmar

1
简单的答案是,就编译而言,是的,这会增加一些开销,但在运行时它只会产生微不足道的差异。原因是,假设您添加了#include <iostream>(仅举例),并且假设您没有使用其中的任何函数,则g++ 4.5.2需要处理额外的18,560行代码(编译)。但就运行时开销而言,我几乎认为它不会对性能造成影响。
您还可以参考Are unused includes harmful in C/C++?,我真的很喜欢David Young所说的观点:
任何在头文件中声明为外部的单例,并在源文件中定义的单例都将包含在程序中。这显然会增加内存使用量,并可能通过导致访问页面文件更频繁来导致性能开销(现在不太成问题,因为单例通常大小为小到中等大小,并且因为我认识的大多数人都有6+ GB的RAM)。

1
很有趣,你用#include <iostream>作为例子,因为在运行时包含该头文件确实会产生一些小的开销。 - T.C.
@T.C.:确实,但我只是想表达一个观点。也许我可以添加#include <boost/asio.hpp>,它有大约74k行。 - Rahul Tripathi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接