我对一些研究或经验数据非常感兴趣,这些数据显示了两个c ++项目之间的编译时间的比较,除了一个在可能的情况下使用前向声明而另一个使用none之外。
与完全包含相比,转发声明会如何彻底改变编译时间?
#include "myClass.h"
VS
class myClass;
是否有任何研究可以检验这一点?
我意识到这是一个模糊的问题,很大程度上取决于项目。我不希望得到答案的难数。相反,我希望有人可以指导我做一个关于此的研究。
我特别担心的项目有大约1200个文件。每个cpp平均包含5个头。每个标头平均包含5个标头。这回归了大约4个层次。似乎对于编译的每个cpp,必须打开和解析大约300个头文件,有时多次。 (包含树中有许多重复项。)有警卫,但文件仍然打开。每个cpp都是用gcc单独编译的,所以没有头缓存。
为了确保没有人误解,我当然主张在可能的情况下使用前瞻性声明。但是,我的雇主禁止他们。我试图反对这个立场。
感谢您提供任何信息。
答案 0 :(得分:16)
前向声明可以使更简单易懂的代码成为任何决策的目标。
认为,当涉及到类时,2个类很可能相互依赖,这使得在不引起噩梦的情况下使用前向声明有点困难。
在标头中同样向前声明类意味着您只需要在实际使用这些类的CPP中包含相关标头。这实际上减少了编译时间。
编辑:鉴于您的评论,我会指出包含头文件比转发声明要慢。每当你包含一个标题时,你需要从磁盘加载,通常只是发现标题保护意味着没有任何反应。这会浪费大量的时间,实际上是一个非常愚蠢的规则。
编辑2 :很难获得硬数据。有趣的是,我曾经参与过一个对标题包含不严格的项目,在512MB RAM P3-500Mhz上的构建时间大约是45分钟(这是一段时间了)。在花了两周时间减少包含噩梦(通过使用前向声明)后,我设法在不到4分钟的时间内构建代码。随后,尽可能使用前向声明成为规则。
编辑3 :同样值得注意的是,在对代码进行少量修改时,使用前向声明有很大的优势。如果整个商店都包含标题,那么对头文件的修改可能会导致重建大量文件。
我还注意到很多其他人在赞美预编译头文件(PCHs)的优点。他们有他们的位置,他们可以真正帮助,但他们真的不应该用作正确的前方声明的替代。否则,对头文件的修改可能会导致重新编译大量文件(如上所述)以及触发PCH重建时出现问题。 PCHs可以为像预建的图书馆这样的东西提供巨大的胜利,但他们没有理由不使用正确的前向声明。
答案 1 :(得分:9)
看一下John Lakos出色的Large Scale C++ Design书 - 我认为他有一些前瞻性声明的数字,看看如果你包含N个M级深度会发生什么。
如果不使用前向声明,那么除了增加来自干净源树的总构建时间之外,它还会大大增加增量构建时间,因为不必要地包含头文件。假设你有4个类,A,B,C和D.在实现中使用A和B (即在C.cpp
中),D在其实现中使用C.由于这种“无前瞻性声明”规则,D的界面被迫包括C.h.类似地,C.h被强制包括A.h和B.h,因此每当A或B改变时,即使它没有直接依赖性,也必须重建D.cpp。随着项目的扩展,这意味着如果你触摸任何标题,它将对重建大量代码产生巨大影响,而这些代码根本不需要。
有一个不允许前方声明的规则(在我的书中)确实是非常糟糕的做法。它将浪费大量时间让开发人员无益。一般的经验法则应该是,如果B类的接口依赖于A类,那么它应该包括A.h,否则向前声明它。在实践中,“依赖”意味着继承,用作成员变量或“使用任何方法”。 Pimpl习惯用法是一种广泛且易于理解的方法,用于从接口隐藏实现,并允许您大大减少代码库中所需的重建量。
如果您无法找到Lakos的数据,那么我建议您创建自己的实验并采取时机向您的管理层证明此规则绝对是错误的。
答案 2 :(得分:4)
#include "myClass.h"
是1..n行
class myClass;
是1行。
除非所有标题都是1个内衬,否则您将节省时间。因为对编译本身没有影响(前向引用只是向编译器说明在链接时定义特定符号的方式,并且只有在编译器不需要来自该符号的数据时才有可能(例如数据大小) )),每次通过前向引用替换一个文件时,将保存所包含文件的读取时间。这是一个常规的衡量标准,因为它是每个项目的价值,但它是大型c ++项目的推荐做法(有关使用c ++管理大型项目的技巧的更多信息,请参阅Large-Scale C++ Software Design / John Lakos他们已过时了)
限制编译器在头文件上传递的时间的另一种方法是预编译头文件。
答案 3 :(得分:2)
你问过一个非常普遍的问题,它引出了一些非常好的一般答案。但你的问题不是关于你的实际问题:
为了确保没有人误解,我当然主张在可能的情况下使用前瞻性声明。但是,我的雇主禁止他们。我试图反对这个立场。
我们有一些关于该项目的信息,但还不够:
我特别担心的项目有大约1200个文件。每个cpp平均包含5个头。每个标头平均包含5个标头。这回归了大约4个层次。似乎对于编译的每个cpp,必须打开和解析大约300个头文件,有时多次。 (包含树中有许多重复项。)有警卫,但文件仍然打开。每个cpp都是用gcc单独编译的,所以没有头缓存。
你对使用gcc的预编译头文件做了什么?它在编译时有什么不同?
现在编译干净的构建需要多长时间?您的典型(非清洁/增量)构建多长时间?如果像James McNellis在评论中的例子那样,建立时间不到一分钟:
我工作的最后一个大型C ++项目是100万SLOC(不包括第三方库)。 ...我们根本没有使用前向声明,整个事情在10分钟内完成。增量重建大约为秒。
然后通过避免包含来节省多少时间并不重要:对于许多项目来说,削减秒数建立肯定无关紧要。
获取项目的一小部分代表性部分并将其转换为您希望的项目。测量该样本的未转换版本和转换版本之间的编译时间差异。请记住触摸(或相当于make --assume-new)各种文件集来表示您在工作时遇到的真实构建。
显示 您的雇主如何提高工作效率。
答案 4 :(得分:1)
在任意场景中,我认为翻译单元不会变得更短,更容易编译。前瞻性声明最重要的意图是为程序员提供便利。
答案 5 :(得分:0)
我做了一个小型演示,该演示生成了人工代码库并测试了该假设。
它生成200个标头。每个标头都有一个结构,该结构包含100个字段,注释长度为5000个字节。 500个.c
文件用于基准测试,每个文件包含所有头文件或正向声明所有类。
为了更真实,每个标头也都包含在自己的.c
文件中
结果是,使用include花费了我 22秒,而使用前向声明花费了 9秒。
generate.py
#!/usr/bin/env python3
import random
import string
include_template = """#ifndef FILE_{0}_{1}
#define FILE_{0}_{1}
{2}
//{3}
struct c_{0}_{1} {{
{4}}};
#endif
"""
def write_file(name, content):
f = open("./src/" + name, "w")
f.write(content)
f.close()
GROUPS = 200
FILES_PER_GROUP = 0
EXTRA_SRC_FILES = 500
COMMENT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5000))
VAR_BLOCK = "".join(["int var_{0};\n".format(k) for k in range(100)])
main_includes = ""
main_fwd = ""
for i in range(GROUPS):
include_statements = ""
for j in range(FILES_PER_GROUP):
write_file("file_{0}_{1}.h".format(i,j), include_template.format(i, j, "", COMMENT, VAR_BLOCK))
write_file("file_{0}_{1}.c".format(i,j), "#include \"file_{0}_{1}.h\"\n".format(i,j))
include_statements += "#include \"file_{0}_{1}.h\"\n".format(i, j)
main_includes += "#include \"file_{0}_{1}.h\"\n".format(i,j)
main_fwd += "struct c_{0}_{1};\n".format(i,j)
write_file("file_{0}_x.h".format(i), include_template.format(i, "x", include_statements, COMMENT, VAR_BLOCK))
write_file("file_{0}_x.c".format(i), "#include \"file_{0}_x.h\"\n".format(i))
main_includes += "#include \"file_{0}_x.h\"\n".format(i)
main_fwd += "struct c_{0}_x;\n".format(i)
main_template = """
{0}
int main(void) {{ return 0; }}
"""
for i in range(EXTRA_SRC_FILES):
write_file("extra_inc_{0}.c".format(i), main_includes)
write_file("extra_fwd_{0}.c".format(i), main_fwd)
write_file("maininc.c", main_template.format(main_includes))
write_file("mainfwd.c", main_template.format(main_fwd))
run_test.sh
#!/bin/bash
mkdir -p src
./generate.py
ls src/ | wc -l
du -h src/
gcc -v
echo src/file_*_*.c src/extra_inc_*.c src/mainfwd.c | xargs time gcc -o fwd.out
rm -rf out/*.a
echo src/file_*_*.c src/extra_fwd_*.c src/maininc.c | xargs time gcc -o inc.out
rm -rf fwd.out inc.out src
Results
$ ./run_test.sh
1402
8.2M src/
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 11.0.3 (clang-1103.0.32.29)
Target: x86_64-apple-darwin19.3.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
22.32 real 13.56 user 8.27 sys
8.51 real 4.44 user 3.78 sys