使用Google mock时,g++编译多个文件比单个文件慢得多。

4
我遇到了一个似乎与g++有关的问题。基本上,将程序拆分成多个文件比一个单一的文件需要更多时间来编译。实际上,如果您将各个文件连接在一起并进行编译,它会比在g++命令行中列出各个文件运行得更快。例如,使用九个文件时,需要1分钟39秒才能编译;当我将它们全部合并在一起时,只需要13秒即可编译。我尝试过使用strace,但它仅停留在cc1plus上; 当我使用-f选项时,我仍然无法找出问题所在。

我已经确定了问题。以下是如何重现它。我编写了一个非常简单的程序,如下所示:

void func_01(int i) 
{
  int j;
  volatile int *jp;

  jp = &j;

   for (; i; i--) ++*jp;
}

void call_01(void)
{
  func_01(10000);
}

int main(int argc, char *argv[])
{
  call_01();
}

然后,我复制了它,删除了主要部分并替换了逐渐增加的数字,共进行了999次。然后我构建了:

% time g++ -c test*.cpp

real    0m18.919s
user    0m10.208s
sys     0m5.595s
% cat test*.cpp > mon.cpp
% time g++ -c mon.cpp  

real    0m0.824s
user    0m0.776s
sys     0m0.040s

因为我计划扩展到比这更加复杂的数百个文件,所以降低构建时间非常重要。有没有人能够帮忙解释一下这是为什么,或者提供一个不那么粗糙的解决方法?我认为这与预处理器和包含保护所带来的节约有关,因为如果我包括一个文件,时间差异会显著增加(在一个案例中增加了五倍),但如果不包括,则使用单体文件仍然比它快 20 倍。

g++ 的版本是 4.4.2,但我检查了最新版本 8.2.0,结果也存在这个问题。


1
很遗憾,我怀疑你在这里得不到答案。C++代码的编译时间(特别是当打开优化时,而你没有)被认为是很高的,人们倾向于学会与之共存。 - SergeyA
只是一个有教养的猜测:在大文件中,编译器可以看到除了 call_01()func_01(int i) 之外没有使用任何东西,因此它可以优化掉所有其他代码。在多文件方法中,它无法这样做。所以它必须为所有代码生成代码,然后将其留给链接器决定哪些内容进入二进制文件。 - NathanOliver
1
你尝试过使用预编译头文件吗?例如像这样的:$(CXX) $(CPPFLAGS) $(DEFINES) -c src/stdafx.hpp -o $(OBJ)/stdafx.hpp.gch$(OBJ)/%.o: src/%.cpp $(CXX) $(CPPFLAGS) $(DEFINES) -iquoteobj$(OBJ)/stdafx.hpp.gch -c $< -o $@在你的make文件中。这是一个示例make文件 - Victor Gubin
不要忘记,对于许多小文件,您可以并行编译它们。 - Anty
@eewanco @Anty 使用 make -j <N> 命令进行编译,其中 N 为并行进程数。CPU 核心数(超线程的线程数)是选择此参数的好方法,例如 make -j 8。但在重新编译头文件的情况下,必须首先编译它,然后再编译其他文件,因此 make -j <N> 可能与此选项不兼容,因为没有任何排序保证。这种构建还可能会使您的系统长时间使用 100% 的 CPU 使用率。 - Victor Gubin
显示剩余2条评论
2个回答

5

有两种不同的效应:

  1. 编译器调用开销:编译器是复杂的可执行文件,有时甚至会分成前端和后端可执行文件,前端为每个单独的源文件产生后端,即使所有源文件都被传递给同一编译器前端的编译器调用。例如,gcc和llvm就是这样做的。

    • 指定 g++ -v 以查看这些冗余的编译器调用。我认为这回答了你的主要问题,即为什么即使没有头文件也会出现这种情况。
  2. 由于重复解析和编译相同的标头文件而产生的开销。在真实世界的例子中,这个头文件开销将比编译器调用本身更加显著。

因为如果我包含一个文件,时间差异会急剧增加(在一个案例中增加了5倍)

是的!这可能会比5倍慢1000倍。对于模板密集代码,编译器在编译时需要做很多工作。

当将程序拆分到许多源文件时,尤其是针对C++代码,会遇到减速问题,因为C++需要大量使用头文件。所有源码的*.cpp都会被单独编译,并且它们包含的所有头文件都会针对每个单独的源文件重复调用。

现在,如果您将所有源文件连接在一起,则所有标头仅解析一次,因为有了包含保护。由于编译器花费大部分时间来解析和编译头文件,这非常重要,特别是对于使用STL等模板密集代码(例如)的情况。

手写的C++源代码和生成的C++源代码的数量是以下权衡:

  1. 我的完整重建时间很快,但我的增量构建时间很慢。

    • 当您只有一个源文件(即*.cpp文件)或非常少的源文件时,就会出现这种情况。
  2. 我的完整构建时间很慢,但我的增量构建时间很快。

    • 当您有许多小型源文件(即*.cpp文件)时,就会出现这种情况。

(在任何情况下,头文件的数量并不重要(除非您总是拉入太多的冗余内容。这是关于编译器调用的数量,这是*.cpp或*.o文件的数量。)

对于1.从头开始的完整编译时间很短,因为编译器只看到所有头文件一次,在C++中非常重要,特别是对于基于模板的仅标头(或密集型标头)库,如STL或boost。

对于2.只有在单个数百个文件更改时,才会编译*.cpp文件中的极少量代码,因此单个编译时间很快。

这强烈取决于您的用例。

如果您生成C++代码,则应向您的生成器添加选项,以允许用户选择如何处理此权衡。


1
头文件的数量并不是很重要。如果在指定给编译器的包含路径中有太多的路径,这可能会对编译时间产生很大影响。 - drescherjm
OP的示例没有头文件。 - SergeyA
@SergeyA 这是真的,但我的原始问题确实涉及头文件。昨晚我想到,也许我陷入了试图隔离一个示例的泥潭中 - 有1,000个文件,仅文件访问开销就很可能解释时间差异。我看到 Bo R 在另一个答案中也得出了这个结论。 - Vercingatorix

2
我认为在这种情况下,大部分的开销来自于打开和关闭文件。在两种情况下,都只有一个进程在执行任务。
我做了一个比较,使用 "cat" 程序将结果转储到 /dev/null。对于 "cat test_*.cpp >/dev/null",它需要约0.008秒,而对于 "cat mon.cpp >/dev/null",则需要约0.001秒。对于999个文件,这几乎是10倍的差异。此外,编译器还必须为其编译的每个文件设置一些内部管理,而这只针对大型单体案例进行一次。
但是,正如其他人已经回答的那样,在构建系统(如make或ninja)中设置构建时,仅涉及多个文件之一时,与单个文件相比,差异就会变得明显。使用ninja进行重建,单体案例需要1.196秒,而999个文件的情况只需要0.233秒。
注:这些数字中没有明确的头文件。

1
可能真正的开销是一个编译器进程与旋转999个编译器进程之间的差异,以及编译每个文件的开销。 - valiano
@valiano 最初的示例都没有并发编译。我也没有这样做。但我尝试过使用ninja和make,并在我的4核机器上使用-j6标志运行。 - Bo R
@BoR,昨晚我得出了同样的结论,我认为我的示例是一个误导。真正的罪魁祸首是头文件重新处理的开销。我从未听说过ninja;你有什么好的资源可以方便地提供吗?谷歌搜索“ninja”可能不会有所帮助哈哈。 - Vercingatorix
@eewanco 你可以尝试使用 https://ninja-build.org/,但你可能需要使用 CMake 生成它的文件。因此,在 Linux 系统上安装时,只需查找 ninja-build 即可。 - Bo R

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