关于仅使用标头的c ++库的使用量化指标(基准)

Hom*_*er6 41 c++ benchmarking header-only

我试图用SO找到答案.有许多问题列出了在c ++中构建一个只有头文件库的各种优缺点,但是我无法找到一个以可量化的术语来构建这样的库.

因此,在可量化的术语中,使用传统上分离的c ++标头和实现文件与仅标头之间有什么不同?

为简单起见,我假设不使用模板(因为它们只需要标题).

详细说明,我列出了我从文章中看到的优点和缺点.显然,有些不容易量化(例如易用性),因此无法用于量化比较.我会用可量化的方式标记那些我期望可量化的指标.

仅限标题的优点

  1. 由于您不需要在构建系统中指定链接器选项,因此更容易包含.
  2. 您始终使用与代码的其余部分相同的编译器(选项)编译所有库代码,因为库的函数在代码中内联.
  3. 它可能要快得多.(定量的)
  4. 可以为编译器/链接器提供更好的优化机会(如果可能,可以解释/量化)
  5. 如果你仍然使用模板是必需的.

仅限标题的缺点

  1. 它使代码膨胀.(可量化的)(这会如何影响执行时间和内存占用)
  2. 编译时间更长.(定量的)
  3. 失去接口和实现的分离.
  4. 有时会导致难以解决的循环依赖关系.
  5. 防止共享库/ DLL的二进制兼容性.
  6. 它可能会加剧那些喜欢使用C++的传统方式的同事.

您可以从较大的开源项目(比较类似大小的代码库)中使用的任何示例都非常感激.或者,如果您知道可以在仅标题版本和分离版本之间切换的项目(使用包含两者的第三个文件),那将是理想的.轶事数字也很有用,因为它们给了我一个可以获得一些洞察力的球场.

利弊来源:

提前致谢...

更新:

对于可能稍后阅读并且有兴趣获得关于链接和编译的一些背景信息的任何人,我发现这些资源很有用:

更新:(回应下面的评论)

仅仅因为答案可能不同,并不意味着测量是无用的.你必须开始测量一些点.您拥有的测量值越多,图像就越清晰.我在这个问题上要求的不是整个故事,而是对图片的一瞥.当然,如果他们想要不道德地宣传他们的偏见,任何人都可以使用数字来扭曲争论.但是,如果有人对两个选项之间的差异感到好奇并发布这些结果,我认为这些信息很有用.

没有人对这个话题感到好奇,足以衡量它吗?

我喜欢枪战项目.我们可以从删除大部分变量开始.只在一个版本的linux上使用一个版本的gcc.仅对所有基准测试使用相同的硬件.不要使用多个线程进行编译.

然后,我们可以测量:

  • 可执行大小
  • 运行
  • 内存占用
  • 编译时间(对于整个项目和更改一个文件)
  • 链接时间

Rea*_*law 30

摘要(值得注意的一点):

  • 基准测试的两个包(一个包含78个编译单元,一个包含301个编译单元)
  • 传统编译(多单元编译)使应用程序的速度提高了7%(在78单元包中); 301单元包中的应用程序运行时没有变化.
  • 传统编译和仅基于标头的基准测试在运行时(在两个包中)使用相同数量的内存.
  • 仅标题编译(单个单元编译)导致301单元包中的可执行文件大小减小10%(78单元包中仅小1%).
  • 传统编译使用大约三分之一的内存来构建两个包.
  • 传统编译花费了三倍的时间来编译(在第一次编译时),并且在重新编译时只占用了4%的时间(因为只有头文件必须重新编译所有源代码).
  • 传统编译在第一次编译和后续编译时需要更长的时间.

Box2D基准,数据:

box2d_data_gcc.csv

Botan基准,数据:

botan_data_gcc.csv

Box2D摘要(78个单位)

在此输入图像描述

Botan概要(301个单位)

在此输入图像描述

好的图表:

Box2D可执行文件大小:

Box2D可执行文件大小

Box2D编译/链接/构建/运行时间:

Box2D编译/链接/构建/运行时

Box2D编译/链接/构建/运行最大内存使用情况:

Box2D编译/链接/构建/运行最大内存使用量

Botan可执行文件大小:

Botan可执行文件大小

Botan编译/链接/构建/运行时间:

Botan编译/链接/构建/运行时

Botan编译/链接/构建/运行最大内存使用情况:

Botan编译/链接/构建/运行最大内存使用量


基准详情

TL; DR


测试的项目,Box2DBotan之所以被选中,是因为它们的计算成本可能很高,包含大量单元,并且实际上很少或没有错误编译成一个单元.尝试了许多其他项目,但是花费了太多时间来"修复"作为一个单元进行编译.通过定期轮询内存占用并使用最大值来测量内存占用量,因此可能不完全准确.

此外,此基准测试不会执行自动标头依赖关系生成(以检测标头更改).在使用不同构建系统的项目中,这可能会增加所有基准测试的时间.

基准测试中有3个编译器,每个编译器有5个配置.

编译:

  • GCC
  • ICC

编译器配置:

  • 默认 - 默认编译器选项
  • 优化原生 - -O3 -march=native
  • 尺寸优化 - -Os
  • LTO/IPO原生 - -O3 -flto -march=native与clang和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,clang,icc/icpc.当然可以修改以删除任何这些.
  • 每个基准测试都应该有一个数据文件,列出该基准测试的单位.然后,run.py将创建两个测试用例,一个单独编译每个单元,另一个单元编译在一起.示例:box2d.data.文件格式定义为json字符串,包含具有以下键的字典
    • "units"- c/cpp/cc组成该项目单位的文件列表
    • "executable" - 要编译的可执行文件的名称.
    • "link_libs" - 要链接到的已安装库的空格分隔列表.
    • "include_directores" - 要包含在项目中的目录列表.
    • "command"- 可选的.执行特殊命令以运行基准测试.例如,"command": "botan_test --benchmark"
  • 并非所有C++项目都可以轻松完成; 单个单元中不得有任何冲突/含糊之处.
  • 要将项目添加到测试的情况下,修改该列表test_base_casesrun.py与信息工程,包括数据文件名.
  • 如果一切运行良好,输出文件data.csv应包含基准测试结果.

要生成条形图:

  • 您应该从基准测试生成的data.csv文件开始.
  • 获取chart.py.需要matplotlib.
  • 调整fields列表以决定生成哪些图形.
  • python chart.py data.csv.
  • 一个文件,test.png现在应该包含结果.

Box2D的

  • Box2D 原样使用svn,修订版251.
  • 该基准取自这里,修改这里,可能不能代表一个很好的Box2D的标杆,它可能不会使用的Box2D的,足以做到这一点的编译器基准正义.
  • 通过查找所有.cpp单位手动编写box2d.data文件.

牡丹

  • 使用Botan-1.10.3.
  • 数据文件:botan_bench.data.
  • 首先运行./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.
  • 我禁用了程序集,因为程序集可能会干扰当函数边界不阻止优化时可能发生的优化.然而,这是猜想,可能完全错误.
  • 然后运行命令grep -o "\./src.*cpp" Makefile和,grep -o "\./checks.*" Makefile以获取.cpp单位并将它们放入botan_bench.data文件中.
  • 修改/checks/checks.cpp为不调用x509单元测试,并删除了x509检查,因为Botan typedef和openssl之间存在冲突.
  • 使用了Botan源中包含的基准.

系统规格:

  • OpenSuse 11.4,32位
  • 4GB RAM
  • Intel(R) Core(TM) i7 CPU Q 720 @ 1.60GHz


Rea*_*law 28

更新

这是Real Slaw的原始答案.他上面的回答(被接受者)是他的第二次尝试.我觉得他的第二次尝试完全回答了这个问题. - 荷马6

那么,为了比较,你可以查找"统一构建"的想法(与图形引擎无关).基本上,"统一构建"是将所有cpp文件包含在单个文件中,并将它们全部编译为一个编译单元的地方.我认为这应该提供一个很好的比较,因为AFAICT,这相当于只使你的项目标题.你对你列出的第二个"骗局"感到惊讶; "统一构建"的重点是减少编译时间.据说统一构建编译速度更快,因为它们:

..是一种减少构建开销的方法(特别是通过减少生成的目标文件的数量来打开和关闭文件并减少链接时间),因此用于大幅加快构建时间.

- altdevblogaday

编译时间比较(从这里):

在此输入图像描述

"团结建设"的三个主要参考:

我假设您想要列出利弊的原因.

仅限标题的优点

[...]

3)它可能要快得多.(可量化的)代码可能会更好地进行优化.原因是,当单元是分开的时,函数只是一个函数调用,因此必须保留.没有关于此呼叫的信息,例如:

  • 这个函数会修改内存吗(因此我们反映这些变量/内存的寄存器在返回时会过时)吗?
  • 这个函数是否会查看全局内存(因此我们无法重新排序调用函数的位置)
  • 等等

此外,如果函数内部代码已知的,则可能值得内联它(即将其代码直接转储到调用函数中).内联避免了函数调用开销.内联还允许发生一系列其他优化(例如,常量传播;例如我们调用factorial(10),现在如果编译器不知道代码factorial(),则被迫离开它,但如果我们知道源代码代码factorial(),我们实际上可以变量函数中的变量并用10替换它,如果我们很幸运,我们甚至可以在编译时得到答案,而不在运行时运行任何东西).内联后的其他优化包括死码消除和(可能)更好的分支预测.

4)可以为编译器/链接器提供更好的优化机会(如果可能,可以解释/量化)

我认为这是从(3)开始的.

仅限标题的缺点

1)它使代码膨胀.(可量化的)(这会如何影响执行时间和内存占用量)只有Header可以通过几种方式使代码膨胀,我知道.

首先是模板膨胀; 编译器实例化从未使用过的类型的不必要模板.这不仅仅是标题,而是模板,现代编译器对此进行了改进,使其成为最小的问题.

第二个更明显的方法是(过)内联函数.如果在任何地方使用大型函数,那么这些调用函数的大小会增加.这可能是几年前关于可执行文件大小和可执行映像内存大小的问题,但硬盘驱动器空间和内存已经增长,使得它几乎毫无意义.更重要的问题是,这种增加的函数大小会破坏指令缓存(因此现在更大的函数不适合缓存,现在缓存必须在CPU通过函数执行时重新填充).内联后寄存器压力会增加(寄存器数量有限制,CPU可以直接处理的CPU内存).这意味着编译器必须在现在更大的函数中间处理寄存器,因为变量太多了.

2)编译时间更长.(定量的)

好吧,只有标题编译可以在逻辑上导致更长的编译时间,原因有很多(尽管有"统一构建"的表现;逻辑不一定是现实世界,其他因素也参与其中).一个原因可能是,如果整个项目只是标题,那么我们就会失去增量构建.这意味着项目任何部分的任何更改都意味着必须重建整个项目,而使用单独的编译单元时,一个cpp中的更改只意味着必须重建目标文件,并重新链接项目.

在我的(轶事)经历中,这是一个很大的打击.在某些特殊情况下,标题只能提高性能,但生产效率明显,通常不值得.当您开始获得更大的代码库时,从头开始编译时间每次大约需要10分钟.在微小的变化上重新编译开始变得令人厌烦.你不知道有多少次我忘记了";" 并且不得不等待5分钟才能听到它,只是回去修理它,然后等待另外5分钟,通过修复";"再找到我刚才介绍的其他东西.

性能很好,生产力更好; 它会浪费你很大一部分时间,并使你的编程目标失去动力/分散注意力.

编辑:我应该提一下,过程间优化(参见链接时优化整个程序优化)试图完成"统一构建"的优化优势.在大多数编译器AFAIK中,这种实现仍然有点摇摇欲坠,但最终这可能会克服性能优势.

  • 好的帖子,但是统一构建可能与使用仅头文件库的单独编译有很大不同.在统一构建中,标头依赖性围栏通常会阻止标头内容被多次包含.您只能获得一个包含文件代码的副本.使用单独的编译和仅包含库,您最终可能会得到一个库_per编译单元的副本.因此OP描述的代码膨胀. (3认同)