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

时间:2012-09-05 22:23:55

标签: c++ benchmarking header-only

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

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

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

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

仅限标题的优点

  1. 包含更容易,因为您不需要在构建系统中指定链接器选项。
  2. 您总是使用与代码的其余部分相同的编译器(选项)编译所有库代码,因为库的函数在您的代码中被内联。
  3. 它可能要快得多。 (定量的)
  4. 可以为编译器/链接器提供更好的优化机会(如果可能,可以解释/量化)
  5. 无论如何都需要使用模板。
  6. 仅限标题的缺点

    1. 它使代码膨胀。 (可量化的)(这会如何影响执行时间和内存占用量)
    2. 编译时间更长。 (定量的)
    3. 失去界面和实施的分离。
    4. 有时会导致难以解决的循环依赖。
    5. 防止共享库/ DLL的二进制兼容性。
    6. 这可能会加剧那些喜欢使用C ++的传统方式的同事。
    7. 您可以从较大的开源项目(比较类似大小的代码库)中使用的任何示例都将非常感激。或者,如果您知道可以在仅标题版本和分离版本之间切换的项目(使用包含两者的第三个文件),那将是理想的。轶事数字也很有用,因为它们给了我一个可以获得一些洞察力的球场。

      利弊的来源:

      提前致谢...

      更新

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

      更新:(响应以下评论)

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

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

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

      然后,我们可以测量:

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

3 个答案:

答案 0 :(得分:30)

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

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

Box2D基准,数据:

box2d_data_gcc.csv

Botan基准,数据:

botan_data_gcc.csv

Box2D摘要(78个单位)

enter image description here

Botan摘要(301个单位)

enter image description here

NICE CHARTS:

Box2D可执行文件大小:

Box2D executable size

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

Box2D compile/link/build/run time

Box2D compile / link / build / run max memory usage:

Box2D compile/link/build/run max memory usage

Botan可执行文件大小:

Botan executable size

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

Botan compile/link/build/run time

Botan compile / link / build / run max memory usage:

Botan compile/link/build/run max memory usage


基准详情

<强> TL; DR


选择了测试的项目Box2DBotan,因为它们的计算成本可能很高,包含大量单元,实际上很少或没有错误编译为单个单元。许多其他项目都在尝试,但耗费了太多时间来修复&#34;编译为一个单元。通过定期轮询内存占用并使用最大值来测量内存占用量,因此可能不完全准确。

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

基准测试中有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,所以我们可能会看到&#34;正确&#34;实现单位效率比较的方法。

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 - 听起来像。

重现基准:

  • 斗牛是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 ++项目都可以轻松完成;单个单元中不得有任何冲突/含糊之处。
  • 要将项目添加到测试用例,请使用项目信息修改run.py中的列表test_base_cases,包括数据文件名。
  • 如果一切运行良好,输出文件data.csv应包含基准测试结果。

制作条形图:

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

Box2D的

  • Box2D用于svn as is,修订版251。
  • 基准测试取自here,修改后的here,可能无法代表优秀的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" Makefilegrep -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

答案 1 :(得分:28)

<强>更新

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

嗯,为了比较,你可以查看&#34;统一构建&#34; (与图形引擎无关)。基本上,&#34;统一构建&#34;将所有cpp文件包含在一个文件中,并将它们全部编译为一个编译单元。我认为这应该提供一个很好的比较,因为AFAICT,这相当于只使你的项目标题。你对第二个&#34; con&#34;你列出了; &#34;统一建立&#34;是减少编译时间。据说统一构建编译速度更快,因为它们:

  

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

     

- altdevblogaday

编译时间比较(来自here):

enter image description here

&#34;统一构建的三个主要参考:

我认为您需要列出利弊的原因。

  

仅限标题的优点

     

[...]

     

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

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

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

  

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

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

  

仅限标题的缺点

     

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

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

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

  

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

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

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

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

修改:我应该提及 interprocedural optimization (另请参阅 link-time optimization { {3}} )试图完成&#34;统一构建的优化优势&#34;。大多数编译器AFAIK的实现仍然有点摇摇欲坠,但最终这可能会克服性能优势。

答案 2 :(得分:4)

我希望这与Realz所说的不太相似。

可执行(/ object)大小:(可执行0%/对象仅在标题上大50%)

我认为头文件中定义的函数将被复制到每个对象中。当谈到生成可执行文件时,我会说很容易删除重复的函数(不知道哪些链接器做/不做这个,我假设大多数都这样做),所以(可能)没有真正的区别可执行的大小,但在对象大小。差异在很大程度上取决于标题中的代码与项目的其余部分有多少。并不是说这些天对象大小真的很重要,除了链接时间。

运行时:(1%)

我认为基本相同(函数地址是函数地址),内联函数除外。我希望内联函数在普通程序中的差异不到1%,因为函数调用确实有一些开销,但与实际对程序进行任何操作的开销相比,这没什么。

内存占用:(0%)

可执行文件中的相同内容=相同的内存占用(在运行时),假设链接器删除了重复的功能。如果没有删除重复的功能,它可以产生很大的不同。

编译时间(对于整个项目和更改一个文件):(对于任何一个文件,整个速度最多可提高50%,对于非标头,速度最高可提高99%)

巨大的差异。更改头文件中的某些内容会导致包含它的所有内容重新编译,而cpp文件中的更改只需要重新创建该对象并重新链接。对于仅头文件库的完全编译,容易慢50%。但是,使用预编译头文件或统一版本,使用仅限头文件库的完整编译可能会更快,但需要重新编译大量文件的一个更改是一个巨大的缺点,我会说这不值得。不经常需要完全重新编译。此外,您可以在cpp文件中包含某些内容但不包含在其头文件中(这可能经常发生),因此,在正确设计的程序(树状依赖结构/模块化)中,更改函数声明或其他内容时(总是需要)对头文件的更改),header-only会导致很多事情要重新编译,但是如果没有标题,你可以大大限制它。

链接时间:(仅限标题的速度提高50%)

对象可能更大,因此处理它们需要更长的时间。可能与文件的大小成线性比例。从我在大型项目中的有限经验(编译+链接时间足够长到实际问题),与编译时间相比,链接时间几乎可以忽略不计(除非你不断进行小的改动和构建,然后我希望你能感受到它,我想这可能经常发生。)