我试图找到使用SO的答案。 有许多的,列出的各种优点和在C ++中建立一个只有头库的利弊问题,但我一直没能找到一个可量化的方面这样做。
因此,在可量化的方面,有什么用传统的分离C ++头文件和实现文件与只有头之间有什么不同?
为简单起见,我假设模板不使用(因为它们只需要头)。
为了详细说明,我列出我从文章是利弊看到。 显然,一些不容易量化的(如易用性),因此是无用的量化比较。 我还是选那些我期待着与一(量化)量化的指标。
优点为仅标头
- 它更容易有,因为你并不需要在你的编译系统来指定链接器选项。
- 你总是编译所有与相同的编译器(选项)为您的代码的其余部分库代码,因为图书馆的功能得到你的代码内联。
- 这可能是快了很多。 (定量的)
- 可以给编译器/连接进行优化更好的机会(解释/量化的,如果可能的话)
- 如果你使用模板反正是必需的。
缺点为仅标头
- 它涨大的代码。 (量化)(如何既影响执行时间和内存占用)
- 较长的编译时间。 (定量的)
- 接口和实现分离的损失。
- 有时会导致难以解决循环依赖。
- 防止共享库/ dll的二进制兼容性。
- 这可能加剧同事谁更喜欢使用C ++的传统方式。
您可以从更大的,开源项目使用的所有示例(比较同样大小的代码库)将是非常赞赏。 或者,如果你知道,可以仅标头和分离版本之间切换的项目(使用第三个文件,其中包括两个),这将是理想的。 因为他们给我,我可以得到一些见解一个大概的零星数字是太有用了。
对于利弊来源:
- https://stackoverflow.com/a/6200793/278976
- https://stackoverflow.com/a/1783905/278976
提前致谢...
更新:
对于任何可能稍后阅读和有兴趣变得有点的背景资料链接和编译,我发现这些资源有用:
- 第7章http://www.amazon.com/Computer-Systems-Programmers-Perspective-Edition/dp/0136108040
- http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html
- http://www.cyberciti.biz/tips/linux-shared-library-management.html
更新:(响应下面的评论)
仅仅因为答案可能会有所不同,并不意味着测量是没用的。 你必须开始测量的一些点。 而更多的测量你,更清晰的画面。 就是我在这个问题询问是不是故事的全部,但画面的一瞥。 当然,任何人都可以用数字来倾斜的说法,如果他们想不道德促进他们的偏见。 但是,如果有人好奇两个选项之间的差异,并公布这些结果,我认为信息是有用的。
有没有人一直好奇这个话题,足以衡量它?
我喜欢枪战项目。 我们可以通过去除大部分这些变量的开始。 仅在Linux的一个版本使用gcc的一个版本。 只有使用相同的硬件为所有的基准。 不要用多线程编译。
然后,我们可以测量:
- 可执行文件的大小
- 运行
- 内存占用
- 编译时间(对于整个项目,并通过改变一个文件)
- 链接时
摘要(值得注意分):
- 两个基准测试包(一个78编译单元,一个有301编译单元)
- 传统的编译(多单位编辑)导致更快的7%应用程序(在78单元包); 在应用程序运行时在301单元包没有变化。
- 传统编译和页眉只有基准(在两个包)运行时使用的存储器的量相同。
- 仅标头编译(单机编译)导致了可执行文件的大小,这是在301单元封装小10%(在78单元封装小只有1%)。
- 使用了大约三分之一的内存来建立在两个包传统编译。
- 传统的编译了三倍的时间来编译(第一编译)并重新编译了只有4%的时间(如头,只需要重新编译所有来源)。
- 传统的编译需要较长的时间对第一份汇编和后续编译两个链路。
Box2D的基准数据:
box2d_data_gcc.csv
牡丹基准数据:
botan_data_gcc.csv
Box2D的内容(78个单位)
牡丹发明内容(301个单位)
NICE图:
Box2D的可执行文件的大小:
Box2D的编译/链接/建设/运行时间:
Box2D的编译/链接/建设/运行最大内存使用:
牡丹可执行文件的大小:
牡丹编译/链接/编译/运行时间:
牡丹编译/链接/编译/运行最大内存使用:
基准详细
TL; DR
该项目测试的Box2D和牡丹被选中,是因为他们是潜在的计算量很大,含有相当数量的单位,实际上有很少或没有错误编译为一个单元。 许多其他项目已尝试但被消耗太多的时间来“修复”成编译为一个单元。 内存占用由轮询测量以规则的间隔内存占用和使用最大,并且因此可能不完全准确。
另外,该基准不执行自动报头相关性生成(以检测报头的改变)。 在使用不同的构建系统项目,这可能会增加时间的所有标准。
有3级的编译器在基准,每个具有5个配置。
编译:
编译器配置:
- 默认 - 默认编译器选项
- 优化原生-
-O3 -march=native
- 大小进行了优化-
-Os
- LTO / IPO本土-
-O3 -flto -march=native
铿锵和gcc, -O3 -ipo -march=native
与ICPC / ICC - 零优化-
-Os
我觉得这些每个人都可以对之间的单单元和多单元构建比较不同的轴承。 我包括LTO / IPO,所以我们可能会看到如何“正确”的方式来实现单单元效益进行比较。
CSV字段的说明:
-
Test Name
-基准的名称。 例如: Botan, Box2D
。 - 测试配置 - 命名这个测试(特殊CXX标志等)的特定配置。 通常情况下,相同
Test Name
。 -
Compiler
-使用的编译器的名称。 例如: gcc,icc,clang
。 -
Compiler Configuration
-名称中使用的编译器选项的配置。 例如: gcc opt native
-
Compiler Version String
-由编译器本身的编译器版本输出的第一道防线。 例如: g++ --version
产生g++ (GCC) 4.6.1
我的系统上。 -
Header only
的价值- True
,如果这个测试用例建成为一个单元, False
如果它建成一个多单元项目。 -
Units
-在测试的情况下单元的数目,即使它被构建为单个单元。 -
Compile Time,Link Time,Build Time,Run Time
-因为它的声音。 -
Re-compile Time AVG,Re-compile Time MAX,Re-link Time AVG,Re-link Time MAX,Re-build Time AVG,Re-build Time MAX
-时代跨越重建触摸一个文件后该项目。 每个单位都感动,并为每个,项目重建。 最大倍,平均时间被记录在这些领域。 -
Compile Memory,Link Memory,Build Memory,Run Memory,Executable Size
-因为他们的声音。
要重现基准:
- 该bullwork是run.py 。
- 需要psutil (对于内存占用测量)。
- 需要gnumake的。
- 正因为如此,需要GCC,铛,ICC /路径ICPC。 可以进行修改,以消除任何对这些课程。
- 每个基准应该有一个数据文件,该文件列出了基准的单位。 run.py然后将创建两个测试用例,一个具有单独编译每个单元,和一个与编在一起每个单元。 例如: box2d.data 。 该文件格式被定义为JSON字符串,含有字典使用下列键
-
"units"
-名单c/cpp/cc
文件,使这项工程的单位 -
"executable"
-可执行文件的名称进行编译。 -
"link_libs"
-安装的库链接到的空间分隔的列表。 -
"include_directores"
-目录的列表,包括在项目中。 -
"command"
-可选。 特殊的命令来执行运行基准。 例如, "command": "botan_test --benchmark"
- 并不是所有的C ++项目可以这样轻松地进行; 必须没有冲突/在单个单元的歧义。
- 要将项目添加到测试的情况下,修改列表
test_base_cases
在run.py与信息工程,包括数据文件名。 - 如果一切运行良好,输出文件
data.csv
应包含的基准测试结果。
为了生产条形图:
- 您应该由基准产生data.csv文件开始。
- 获取chart.py 。 需要matplotlib 。
- 调整
fields
列表来决定哪些图形来产生。 - 运行
python chart.py data.csv
。 - 一个文件,
test.png
现在应该包含的结果。
Box2D的
- Box2D的是从使用SVN的是 ,修订251。
- 该基准取自这里 ,修改这里 ,可能不能代表一个很好的Box2D的标杆,它可能不会使用的Box2D的,足以做到这一点的编译器基准正义。
- 该box2d.data文件手工编写的,通过查找所有的.cpp单位。
牡丹
- 使用牡丹-1.10.3 。
- 数据文件: botan_bench.data 。
- 第一RAN
./configure.py --disable-asm --with-openssl --enable-modules=asn1,benchmark,block,cms,engine,entropy,filters,hash,kdf,mac,bigint,ec_gfp,mp_generic,numbertheory,mutex,rng,ssl,stream,cvc
,这会产生的头文件和Makefile。 - 我禁用组件,因为组件可能会在功能边界不阻止优化,可以occure优化intefere。 然而,这是猜想,可能是完全错误的。
- 然后跑起来像命令
grep -o "\./src.*cpp" Makefile
和grep -o "\./checks.*" Makefile
获得的.cpp单位,把它们放进botan_bench.data文件。 - 修改
/checks/checks.cpp
不叫X509的单元测试,并删除X509检查,因为牡丹的typedef和OpenSSL之间的冲突。 - 使用包括在牡丹源的基准。
系统规格:
- 了OpenSuse 11.4,32位
- 4GB内存
-
Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz
更新
这是真正的色拉的原来的答复。 他的回答以上(接受一个)是他的第二次尝试。 我觉得他的第二次尝试完全回答了这个问题。 - Homer6
那么,对于比较,你可以看看“团结构建”(无关的图形引擎)的想法。 基本上,“团结建设”是你包括所有的cpp文件到一个单一的文件,并编译它们都作为一个编译单元。 我想这应该提供一个良好的比较,因为AFAICT,这相当于使您的项目仅标头。 你会惊奇地发现你列出的第二届“CON”; 整点“团结构建”是减少编译时间。 据说团结构建编译更快,因为它们:
..是减少建立过头部(具体地打开和关闭的文件,并通过减少生成的对象的文件的数量减少链路倍),因此被用来显着地加快生成时间的一种方式。
- altdevblogaday
(从编译时间比较这里 ):
对于“团结构建三大引用:
- http://buffered.io/posts/the-magic-of-unity-builds/
- http://cheind.wordpress.com/2009/12/10/reducing-compilation-time-unity-builds/
- http://www.altdevblogaday.com/2011/08/14/the-evils-of-unity-builds/
我想,你希望上市的利弊原因。
优点为仅标头
[...]
3)这可能是快了很多。 (量化)的代码可能会被优化的更好。 其原因是,当单位是独立的,功能仅仅是一个函数调用,因此必须保持这样。 关于这个呼叫没有信息是已知的,例如:
- 将这个函数修改内存(因此我们的寄存器反映了这些变量/记忆将过时,当它返回)?
- 这是否功能看全局内存(因此我们不能重新排序,我们调用函数)
- 等等
此外,如果函数内部代码是已知的,它可能是值得它内联(即直接抛售其代码到调用函数)。 内联避免了函数调用的开销。 内联也允许发生其他优化整个主机(例如,常量传播;例如我们所说的factorial(10)
现在如果编译器不知道的代码factorial()
它是被迫离开它像,但如果我们知道的源代码factorial()
我们实际上可以变量的函数中的变量,并用10代替它,如果我们幸运的话,我们甚至可以结束了在编译时的回答,不运行在所有东西在运行时)。 内联后,其他优化包括死代码消除(可能)更好的分支预测。
4)可以给编译器/连接进行优化更好的机会(解释/量化的,如果可能的话)
我想从(3)本如下。
缺点为仅标头
1)涨大的代码。 (量化)(如何既影响执行时间和内存占用)页眉只能臃肿的代码在几个方面,我所知道的。
首先是模板膨胀; 当编译器实例从未使用的类型不必要的模板。 这不是特别头只,而是模板和现代编译器都对这一改进,使其最小的关注。
第二个比较明显的方式,是的函数的(过)内联。 如果内联无处不在它使用了大量的功能,这些调用函数的大小会增加。 这可能是一个关于可执行文件的大小和可执行图像存储器大小关心年前,但硬盘空间和内存已经成长为使它几乎毫无意义的关心。 更重要的问题是,这个增加的功能尺寸可以毁掉指令缓存(这样,现在,大功能,不适合高速缓存,现在缓存有要被填充在CPU执行通过函数)。 寄存器压力将内联(有上寄存器的上CPU存储器,该CPU可直接处理的数量,限制)后会增加。 这意味着,编译器必须兼顾寄存器在现在大功能的中间,因为有太多的变数。
2)更长的编译时间。 (定量的)
好吧,头,只编译逻辑上可以导致更长的编译时间的原因有很多(虽然表现“团结构建”的逻辑未必真实世界,而其他因素参与进来)。 一个原因可能是,如果整个项目是信头只,那么我们就丧失增量构建。 这意味着在项目的任何部分的变化意味着整个项目必须重建,而有独立的编译单元,在一个CPP的变化只是意味着该目标文件必须重建,项目重新连接。
在我的(传闻)的经验,这是一个很大的打击。 头,不仅提升了性能很多在某些特殊情况下,但生产力明智的,它通常是不值得的。 当你开始一个更大的代码库,从头编译时间可每次服用> 10分钟。 重新编译上一个微小的变化开始变得无聊。 你不知道有多少次我忘了“;” 而不得不等待5分钟,听到它,只能回去,并修复它,然后再等5分钟,找别的东西,我只是通过固定推出的“;”。
性能是伟大的,工作效率要好得多; 它会浪费一大块的时间,并demotivate /从编程的目标让你分心。
编辑:我应该指出,这间优化 (见链接时优化 ,以及整个程序优化 )试图实现“统一建设”的优化优势。 这个实现仍然是大多数编译器有点不稳据我所知,但最终这可能克服的性能优势。
我希望这不是太类似于Realz的说。
可执行文件(/对象)大小:(可执行0%/只对象到关于头更大的50%)
我假设定义的函数在头文件将被复制到的每个对象。 当谈到生成可执行,我会说这应该是比较容易切割出重复的功能(不知道哪个链接器不/不这样做,我想大多数人),所以(可能)在没有真正的区别可执行文件的大小,但远在对象的大小。 其差值应该在很大程度上依赖于太多的代码实际上是如何在头与该项目的其他部分。 没有目标的尺寸真的很重要,这些天,除了链接时。
运行时间:(1%)
我说基本上相同的(一个函数的地址是一个函数的地址),除了内联函数。 我期望内嵌功能,使比一般的程序中的1%的差异少,因为函数调用确实有一些开销,但这个是没有什么比实际做了什么程序的开销。
内存占用:(0%)
在可执行=相同的内存占用量(在运行期间)相同的东西,假设链接器切出的重复的功能。 如果有相同的功能,不忌食,它可以使完全不同。
编译时间(对于整个项目,并通过改变一个文件):(整个高达50%更快任一个,单高达99%更快仅不报头)
巨大的差异。 在头文件改变的东西引起的一切,包括它重新编译,而在一个CPP文件的变化,只是要求对象必须重新创建和重新连接。 而对于一个完整的编译头只图书馆慢一件容易的50%。 然而,随着预编译头或统一建立,只有头的库完全编译可能会更快,但一个变化需要大量的文件重新编译的是一个巨大的缺点,我想说,使得它不值得。 完全重新编译是不是经常需要。 此外,您可以在一个CPP文件,但它不是在它的头文件(这种情况经常发生),那么,在一个合适的设计方案(树状依存结构/模块),改变一个函数声明或东西的时候(总是需要更改头文件),仅邮件头会导致很多的东西重新编译,但没有头,不仅可以极大地限制了这一点。
链接时间:(高达50%的速度为仅标头)
该物体可能更大,因此会需要更长的时间来处理它们。 大概是线性正比于如何更大的文件。 从我在大项目(如编译+链接时间足够长,实际上很重要)经验有限,链接时间几乎可以忽略不计相比,编译时间(除非你不断的微小变化和建设,那么我希望你会觉得它,我想可能经常发生)。
文章来源: Quantifiable metrics (benchmarks) on the usage of header-only c++ libraries