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

bmvo0sr5  于 2023-04-08  发布在  其他
关注(0)|答案(7)|浏览(247)

我对一些研究或经验数据非常感兴趣,这些数据显示了两个C++项目之间的编译时间比较,这两个项目是相同的,除了一个在可能的情况下使用正向声明,另一个不使用任何声明。
与完全包含相比,前向声明对编译时间的影响有多大?

#include "myClass.h"

对比

class myClass;

是否有任何相关研究?

我意识到这是一个模糊的问题,很大程度上取决于项目。我不指望一个确切的数字来回答。相反,我希望有人能指导我进行有关这方面的研究。
我特别担心的项目大约有1200个文件。每个cpp平均包含5个头文件。每个头文件平均包含5个头文件。这大约倒退了4个层次。看起来对于每个编译的cpp,大约300个头文件必须被打开和解析,有时需要多次。(include树中有很多重复的文件)虽然有保护,但是文件仍然是打开的。每个cpp都是用gcc单独编译的,所以没有header缓存。
为了确保没有人误解,我当然提倡在可能的情况下使用前向声明。然而,我的雇主禁止使用前向声明。我试图反对这种立场。
谢谢你提供的任何信息。

b09cbbtk

b09cbbtk1#

前向声明可以使代码更整洁,更容易理解,这肯定是任何决策的目标。
再加上一个事实,当涉及到类时,两个类很可能相互依赖,这使得不使用向前声明而不引起噩梦有点困难。
在头文件中同样向前声明类意味着你只需要在实际使用这些类的CPP中包含相关的头文件,这实际上减少了编译时间。

编辑:考虑到你上面的评论,我想指出的是,包含头文件总是比转发声明慢。任何时候你包含一个头文件,你都需要从磁盘加载,往往只是为了发现头保护意味着什么都不会发生。这将浪费大量的时间,而且真的是一个非常愚蠢的规则。
编辑2:硬数据是很难获得的。据说,我曾经在一个项目上工作过,这个项目对它的头文件包含并不严格,在512 MB RAM P3- 500 Mhz上的构建时间大约是45分钟(这是一段时间回来)。在花了2个星期削减包括噩梦(通过使用前向声明)我已经设法在不到4分钟的时间内构建了代码。随后,尽可能使用前向声明成为一条规则。
编辑3:同样值得记住的是,在对代码进行小的修改时,使用正向声明有一个巨大的优势。如果头文件在整个车间都包含在内,那么对头文件的修改可能会导致大量文件被重新构建。

我还注意到许多其他人都在赞美预编译头文件的优点(PCH)。它们有自己的位置,它们确实可以提供帮助,但它们真的不应该被用作正确的前向声明的替代品。否则,修改头文件可能会导致重新编译大量文件的问题PCH可以为预构建的库之类的东西提供巨大的优势,但它们没有理由不使用正确的前向声明。

lqfhib0f

lqfhib0f2#

看看John Lakos的优秀的Large Scale C++ Design书--我 * 认为 * 他有一些关于前向声明的数据,看看如果你包括N个头文件,M个级别的深度会发生什么。
如果你不使用正向声明,那么除了增加从一个干净的源代码树的总构建时间外,它还大大增加了增量构建时间,因为不必要地包含了头文件。C和D. C在其实现中使用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的数据,那么我建议你自己做实验,计时,向你的管理层证明这条规则是绝对错误的。

abithluo

abithluo3#

我做了一个小的demo,它生成了人工代码库并测试了这个假设。它生成了200个头文件。每个头文件都有一个100个字段的结构和一个5000字节长的注解。500个.c文件用于基准测试,每个文件包括所有的头文件或forward声明所有的类。为了使它更真实,每个头文件也包含在它自己的.c文件中
结果是使用includes花费了我22秒来编译,而使用forward声明花费了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
k75qkfdt

k75qkfdt4#

#include "myClass.h"

是1..n行

class myClass;

是1行。
除非所有的头文件都是1行程序,否则会保存时间。因为这对编译本身没有影响(前向引用只是对编译器说将在链接时定义特定符号的方式,并且仅当编译器不需要来自该符号的数据(例如数据大小)时才可能),包含的文件的阅读时间将被保存在每次你用前向引用替换一个文件的时候。没有一个常规的测量方法,因为它是每个项目的值,但是对于大型的c项目,这是一个推荐的实践(参见Large-Scale C Software Design / John Lakos,了解更多关于管理c++大型项目的技巧,即使其中一些项目已经过时)
另一种限制编译器在头文件上传递的时间的方法是预编译头文件。

xienkqul

xienkqul5#

你问了一个非常笼统的问题,引出了一些非常笼统的答案。但你的问题并不是关于你的实际问题:
为了确保没有人误解,我当然提倡在可能的情况下使用前向声明。然而,我的雇主禁止使用前向声明。我试图反对这种立场。
我们有一些关于这个项目的信息,但还不够:
我特别担心的项目大约有1200个文件。每个cpp平均包含5个头文件。每个头文件平均包含5个头文件。这大约倒退了4个层次。看起来对于每个编译的cpp,大约300个头文件必须被打开和解析,有时需要多次。(include树中有很多重复的文件)虽然有保护,但是文件仍然是打开的。每个cpp都是用gcc单独编译的,所以没有header缓存。
你对使用gcc的预编译头文件做了什么?它在编译时间上有什么不同?
现在编译一个干净的构建需要多长时间?你的典型(非干净/增量)构建需要多长时间?如果,就像James McNellis在评论中的例子一样,构建时间在一分钟以下:
我工作的最后一个大型C++项目大约有100万个SLOC(不包括第三方库)...我们根本不怎么使用前向声明,整个项目在10分钟内就构建好了。增量重建大约是几秒钟。
那么,避免包含可以节省多少时间并不重要:对于许多项目来说,减少构建的时间肯定不重要。
从你的项目中选取一小部分有代表性的内容,并将其转换成你想要的内容。测量该示例的未转换版本和已转换版本之间的编译时间差异。记住要接触(或相当于make --assume-new)不同的文件集,以代表你在工作时遇到的真实的构建。
向你的老板展示你如何才能更有效率

vltsax25

vltsax256#

嗯,这个问题很不清楚。简单来说,这取决于。
在任意的情况下,我认为翻译单元不会变得更短,更容易编译。向前声明的最重要的目的是为程序员提供方便。

0kjbasz6

0kjbasz67#

对于使用MS Visual Studio的人,请查看Ramon Viladomat的一个名为Compile Score的伟大插件。
它从Clang或MSBuild(pdb)中提取信息,并显示整个构建运行中每个文件操作所需的时间,将前端(预处理器工作)与后端(实际代码生成)分开。您甚至可以看到哪些.cpp文件包含特定的.h,并搜索容易实现的目标以加快构建速度。大量选项和漂亮的功能。如果您有大型项目,值得一试。

相关问题