前向声明对编译时间有多大影响?

Jos*_*shD 22 c++ compilation

我对一些研究或经验数据非常感兴趣,这些数据显示了两个c ++项目之间的编译时间的比较,除了一个在可能的情况下使用前向声明而另一个使用none之外.

与完全包含相比,转发声明会如何彻底改变编译时间?

#include "myClass.h"
Run Code Online (Sandbox Code Playgroud)

class myClass;
Run Code Online (Sandbox Code Playgroud)

有没有研究可以检验这一点?

我意识到这是一个模糊的问题,很大程度上取决于项目.我不希望得到答案的难数.相反,我希望有人可以指导我做一个关于此的研究.

我特别担心的项目有大约1200个文件.每个cpp平均包含5个头.每个标头平均包含5个标头.这回归了大约4个层次.似乎对于编译的每个cpp,必须打开和解析大约300个头文件,有时多次.(包含树中有许多重复项.)有警卫,但文件仍然打开.每个cpp都是用gcc单独编译的,所以没有头缓存.

为了确保没有人误解,我当然主张在可能的情况下使用前瞻性声明.但是,我的雇主禁止他们.我试图反对这个立场.

感谢您提供任何信息.

Goz*_*Goz 16

前向声明可以使更整洁的代码更容易理解,而这些代码肯定是任何决策的目标.

再加上这样一个事实:当涉及到课程时,两个课程很可能相互依赖,这使得在不引起噩梦的情况下使用前向声明变得有点困难.

标头中类的同等前向声明意味着您只需要在实际使用这些类的CPP中包含相关标头.这实际上减少了编译时间.

编辑:鉴于你上面的评论我会指出包含头文件比转发声明总是慢.每当你包含一个标题时,你需要从磁盘加载,通常只是发现标题保护意味着没有任何反应.这会浪费大量的时间,实际上是一个非常愚蠢的规则.

编辑2:很难获得硬数据.有趣的是,我曾经参与过一个对其标题包含不严格的项目,并且在512MB RAM P3-500Mhz上的构建时间大约是45分钟(这是一段时间了).在花了两周时间减少包含噩梦(通过使用前向声明)后,我设法在不到4分钟的时间内构建代码.随后,尽可能使用前向声明成为规则.

编辑3:还值得注意的是,在对代码进行小的修改时,使用前向声明有很大的优势.如果整个商店都包含标题,那么对头文件的修改可能会导致重建大量文件.

我还注意到很多其他人在赞美预编译头文件(PCHs)的优点.他们有他们的位置,他们可以真正帮助,但他们真的不应该被用作正确的前方声明的替代.否则,对头文件的修改可能会导致重新编译大量文件(如上所述)以及触发PCH重建时出现问题.PCHs可以为像预建的图书馆这样的东西提供巨大的胜利,但他们没有理由不使用正确的前向声明.

  • 前向声明如何制作更整洁的代码?我认为它们会显着混淆代码并隐藏依赖关系,使得理解代码变得更加困难. (9认同)
  • Edit2 太棒了。虽然这是轶事,但这仍然比没有好。编辑1:我完全同意。 (2认同)
  • *任何时候你包含一个标题你需要从磁盘加载通常只是发现标题保护意味着没有任何反应.*不正确.如果已经为该TU加载了标头,那么它很可能在OS级文件系统缓存中; 即使不是,编译器也可以识别使用包含保护的标头并优化该情况. (2认同)

the*_*ill 9

看看John Lakos出色的大型C++设计书籍 - 我认为他有一些前瞻性声明的数据,看看如果你包含N个M级深度会发生什么.

如果不使用前向声明,那么除了增加来自干净源树​​的总构建时间之外,它还会大大增加增量构建时间,因为不必要地包含头文件.假设您有4个类,A,B,C和D. C 在其实现中使用A和B (即in C.cpp),D在其实现中使用C. 由于这种"无前向声明"规则,D的界面被强制包含Ch.类似地,Ch被强制包括Ah和Bh,因此每当A或B改变时,即使它没有直接依赖性,也必须重建D.cpp.随着项目的扩展,这意味着如果你触摸任何标题,它将对重建大量代码产生巨大影响,而这是不必要的.

有一个不允许前向声明的规则(在我的书中)确实是非常糟糕的做法.它将浪费大量时间让开发人员无益.一般的经验法则应该是,如果B类的接口依赖于A类,那么它应该包含Ah,否则向前声明它.在实践中,"依赖"意味着继承,用作成员变量或"使用任何方法".Pimpl习惯用法是一种广泛且易于理解的方法,用于从接口隐藏实现,并允许您大大减少代码库中所需的重建量.

如果您无法找到Lakos的数据,那么我建议您创建自己的实验并采取时机向您的管理层证明此规则绝对是错误的.

  • 请注意,_Large Scale C++ Design_于1996年发布.从那时起,编译器性能得到了巨大的改进(最值得注意的是,我认为1996年大多数编译器都不支持预编译头文件). (3认同)
  • @James:是的,预编译头文件和多线程/并行编译器已经走了很长一段路,但是自1996年以来我们公司的代码库也大大增加了.我认为这本书的核心原则今天与当时的相关性一样重要. (2认同)

Art*_*ium 6

我做了一个小演示,它生成人工代码库并测试这个假设。它生成 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 Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

Run Code Online (Sandbox Code Playgroud)


Mat*_*ieu 5

#include "myClass.h"
Run Code Online (Sandbox Code Playgroud)

是1..n行

class myClass;
Run Code Online (Sandbox Code Playgroud)

是1行。

除非所有标头都是1个衬纸,否则您将节省时间。由于对编译本身没有影响(向前引用只是对编译器说,将在链接时定义特定符号,并且仅当编译器不需要该符号中的数据(例如,数据大小)时才有可能)),则每当您用正向引用替换一个文件时,所保存文件的读取时间就会被保存。由于这是每个项目的价值,因此没有常规的衡量标准,但是对于大型c ++项目,建议您采用这种做法(有关如何在c ++中管理大型项目的技巧,请参见“ 大型C ++软件设计 / John Lakos”)。他们过时了)

限制编译器在标头上传递的时间的另一种方法是预编译标头。