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

我试图find这个使用SO的答案。 有很多问题列出了在c ++中构build一个仅包含头文件的库的各种利弊,但是我还没有能够find一个可以量化的文件。

那么,以可量化的术语来说,使用传统分离的c ++头文件和实现文件与头文件仅有区别?

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

详细说明一下,我列举了我从文章中看到的利弊。 显然,有些是不容易量化的(如易用性),因此无法进行量化比较。 我会标记那些我期望可以量化的指标(可量化的)。

优于头只

  1. 包含更容易,因为您不需要在构build系统中指定链接器选项。
  2. 因为库的函数在你的代码中被内联,所以你总是使用与其他代码相同的编译器(选项)来编译所有的库代码。
  3. 这可能会快很多。 (定量的)
  4. 可以给编译器/链接器提供更好的优化机会(如果可能的话,解释/量化)
  5. 如果您使用模板,则是必需的。

缺点仅用于标题

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

任何你可以从更大的开源项目中使用的例子(比较相似大小的代码库)都会非常感激。 或者,如果您知道可以在标题和分隔版本之间切换的项目(使用包含两者的第三个文件),那么这将是理想的。 轶事数字也是有用的,因为他们给我一个球场,我可以得到一些见解。

利弊来源:

  • https://stackoverflow.com/a/6200793/278976
  • https://stackoverflow.com/a/1783905/278976

提前致谢…

更新:

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

  • http://www.amazon.com/Computer-Systems-Programmers-Perspective-Edition/dp/0136108040第7章
  • http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html
  • http://www.cyberciti.biz/tips/linux-shared-library-management.html

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

只是因为答案可能会有所不同,并不意味着测量是无用的。 你必须开始测量某个点。 而你有更多的测量,图片更清晰。 我在这个问题上要求的不是全部,而是一幅画面。 当然,任何人都可以用数字来歪曲论点,如果他们想不道德地推动他们的偏见。 但是,如果有人对两种select之间的差异感到好奇,并公布这些结果,我认为这些信息是有用的。

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

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

那么,我们可以衡量:

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

总结(值得注意的一点):

  • 两个软件包的基准testing(一个是78个编译单元,一个是301个编译单元)
  • 传统的编译(多单元编译)使得应用程序(在78单元包中)速度提高了7%。 301单元包中的应用程序运行时没有改变。
  • 传统编译和仅标头基准testing在运行时都使用相同数量的内存(在这两个软件包中)。
  • 单头编译(Single Unit Compilation)导致301单元包中的可执行文件大小减小了10%(78单元包中只有1%)。
  • 传统编译使用大约三分之一的内存来构build两个包。
  • 传统的编译需要三倍的时间来编译(在第一次编译时),重新编译时只占用了4%的时间(因为头只能重新编译所有的源代码)。
  • 传统的编译在第一次编译和随后的编译上花费了更长的时间。

Box2D基准,数据:

box2d_data_gcc.csv

Botan基准,数据:

botan_data_gcc.csv

Box2D摘要(78个单元)

在这里输入图像说明

Botan概要(301个单位)

在这里输入图像说明

好图:

Box2D可执行文件大小:

Box2D可执行文件大小

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

Box2D编译/链接/生成/运行时间

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

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

Botan可执行文件大小:

Botan可执行文件大小

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

植物编译/链接/建立/运行时间

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

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


基准详细信息

TL; DR


被testing的项目Box2D和Botan被选中是因为它们可能在计算上很昂贵,包含很多单元,实际上很less或没有作为单个单元编译的错误。 许多其他项目都尝试过,但是花费太多时间来“修复”为一个单元编译。 内存占用量是通过定期轮询内存占用量并使用最大值来测量的,因此可能不完全准确。

另外,这个基准testing不会自动生成头部依赖(用于检测头部变化)。 在使用不同构build系统的项目中,这可能会增加所有基准的时间。

基准testing中有3个编译器,每个编译器有5个configuration。

编译:

  • GCC
  • ICC

编译器configuration:

  • 默认 – 默认编译器选项
  • 本地优化 – -O3 -march=native
  • 尺寸优化 – -Os
  • LTO / IPO原生 – -O3 -flto -march=native和clang和gcc, -O3 -ipo -march=native和icpc / icc
  • 零优化 – -Os

我认为这些在单元和多单元构build之间的比较可以有不同的方向。 我包括了LTO / IPO,所以我们可以看到如何实现单一单位效率的“正确”方式。

csv字段的解释:

  • Test Name – 基准的名称。 例如: Botan, Box2D
  • testingconfiguration – 命名此testing的特定configuration(特殊的cxx标志等)。 通常与Test Name相同。
  • Compiler – 使用的编译Compiler名称。 例如: gcc,icc,clang
  • Compiler Configuration – 使用的编译器选项configuration的名称。 例如: gcc opt native
  • Compiler Version String – 从编译器本身输出编译器版本的第一行。 例如: g++ --version在我的系统上生成g++ (GCC) 4.6.1
  • Header only – 如果此testing用例构build为单个单元,则值为True ,如果构build为多单元项目,则为False
  • Units – testing案例中的Units数量,即使它是作为一个单元构build的。
  • 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 – 触摸单个文件后重build项目的时间。 每个单位都被感动,每个项目都被重build。 最大次数和平均次数logging在这些字段中。
  • Compile Memory,Link Memory,Build Memory,Run Memory,Executable Size – 正如它们的声音。

重现基准:

  • 这个牛仔是run.py。
  • 需要psutil (用于内存占用量测量)。
  • 需要GNUMake。
  • 实际上,path中需要gcc,clang,icc / icpc。 可以修改,以删除任何这些当然。
  • 每个基准都应该有一个数据文件,列出这些基准的单位。 然后run.py将创build两个testing用例,每个单元分别编译一个,每个单元编译一个。 例如: box2d.data 。 文件格式被定义为一个jsonstring,包含一个带有下列键的字典
    • "units" – 组成这个项目单位的c/cpp/cc文件的列表
    • "executable" – 要编译的可执行文件的名称。
    • "link_libs" – 链接到已安装库的空格分隔列表。
    • "include_directores" – 包含在项目中的目录列表。
    • "command" – 可选。 执行运行基准的特殊命令。 例如, "command": "botan_test --benchmark"
  • 并不是所有的C ++项目都可以轻松完成; 单位不得有冲突/含糊不清。
  • 要将项目添加到testing用例中, 请使用项目信息(包括数据文件名)修改test_base_cases中的test_base_cases列表。
  • 如果一切运行良好,输出文件data.csv应包含基准testing结果。

要生成条形图:

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

Box2D的

  • Box2D 原样使用svn ,版本251。
  • 基准是从这里拿来的, 在这里修改,可能并不代表一个好的Box2D基准,也可能没有足够的Box2D来做这个编译器基准正义。
  • box2d.data文件是通过查找所有.cpp单元手动编写的。

牡丹

  • 使用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不会调用x509unit testing,并删除x509检查,因为Botan typedef和openssl之间存在冲突。
  • 包含在Botan源文件中的基准被使用。

系统规格:

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

更新

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

那么,为了比较,你可以查看“统一构build”的想法(与graphics引擎无关)。 基本上,“统一构build”就是将所有cpp文件包含到一个文件中,并将它们全部编译为一个编译单元。 我想这应该提供一个很好的比较,因为AFAICT,这相当于使您的项目只有标题。 你会惊讶于你列出的第二个“con”。 “统一build设”的总体目标是缩短编制时间。 据说统一build立编译速度更快,因为他们:

..是一种减less构build开销的方法(特别是打开和closures文件,通过减less生成的对象文件的数量来减less链接时间),并因此用于大幅缩短构build时间。

– altdevblogaday

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

在这里输入图像说明

“统一build设:

我想你想要列出利弊的原因。

优于头只

[…]

3)可能会快很多。 (可量化)代码可能会更好地优化。 原因是,当单位分开时,函数只是一个函数调用,因此必须这样做。 没有关于这个电话的信息是已知的,例如:

  • 这个函数是否会修改内存(并且因此我们的寄存器反映了这些variables/内存在返回时将会陈旧)?
  • 这个函数是否看全局内存(因此我们不能重新sorting我们所说的函数)
  • 等等

此外,如果函数的内部代码已知的,则可能需要将其内联(即将其代码直接转储到调用函数中)。 内联避免了函数调用的开销。 内联还允许发生一系列其他优化(例如,常量传播;例如,我们称之为factorial(10) ,现在如果编译器不知道factorial()的代码,就会被迫像这样离开它,但是如果我们知道factorial()的源代码,我们实际上可以对函数中的variables进行variables并将其replace为10,如果幸运的话,我们甚至可以在编译时结束答案,而不会运行任何东西在运行时)。 内联后的其他优化包括死码消除和(可能)更好的分支预测。

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

我认为从(3)开始。

缺点仅用于标题

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

首先是模板膨胀; 编译器实例化从不使用的不必要的模板。 这不仅仅是头文件而是模板,而现代编译器已经对此进行了改进,使其成为最小的关注点。

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

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

那么,只有头文件的编译可以在逻辑上导致更长的编译时间(尽pipe“统一编译”的性能;逻辑不一定是涉及其他因素的现实世界)。 一个原因可能是,如果整个项目只是标题,那么我们就失去了增量构build。 这意味着项目任何部分的任何改变都意味着整个项目必须被重build,而在单独的编译单元中,一个cpp的改变意味着必须重build目标文件并重新链接项目。

在我的(轶事)的经验,这是一个很大的打击。 在某些特殊情况下,标题只会提高性能,但是在生产效率方面,这通常是不值得的。 当您开始获得更大的代码库时,每次从头开始编译时间可能会大于10分钟。 重新编译一个微小的变化开始变得无聊。 你不知道我有多less次忘了“;” 不得不等待5分钟才能听到,只能回去修理,再等5分钟才能find我刚才介绍的其他东西。

性能很好,生产力要好得多; 它会浪费你大部分的时间,并使你从编程目标中分散/分散注意力。

编辑:我应该提到, 过程间优化 (也参见链接时间优化整个程序优化 )试图完成“统一构build”的优化优势。 在大多数编译器AFAIK中,这个实现仍然有点不稳定,但最终这可能会克服性能优势。

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

可执行文件(/对象)的大小:(可执行文件只有0%/对象,最大可达50%)

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

运行时间:(1%)

我会说基本相同(function地址是一个函数地址),除了内联函数。 我期望内联函数在平均程序中的差异小于1%,因为函数调用确实有一些开销,但是与实际上对程序做任何事情的开销相比,这并不算什么。

内存占用:(0%)

同样的事情在可执行文件=相同的内存占用(运行时),假设链接器切出重复function。 如果重复的function没有被切断,它可能会有很大的不同。

编译时间(对于整个项目和通过更改一个文件):(对于任何一个,整体速度快50%,对于不是仅仅头部,速度快99%)

巨大的差异。 在头文件中改变一些东西会导致包含它的所有东西重新编译,而在一个cpp文件中的改变只需要重新创build对象并重新链接。 对于仅包含头文件的库,完整编译的速度要慢50%。 但是,在预编译头文件或统一构build的情况下,使用仅包含头文件的库进行完整编译可能会更快,但是需要大量文件进行重新编译的更改是一个巨大的缺点,而且我认为这样做不值得。 完全重新编译并不经常需要。 另外,你可以在cpp文件中包含某些东西,但不包含在头文件中(这可能会经常发生),所以在一个合适的devise程序(类似于树的依赖结构/模块化)中,当改变一个函数声明或者某些东西时更改头文件),只有头文件会导致很多事情重新编译,但是不是头文件,只能大大限制这一点。

链接时间:(仅限标题快50%)

对象可能更大,因此处理它们需要更长的时间。 可能与文件的大小成线性比例。 从我在大型项目中的有限经验(编译+链接时间足够长以至于事实上),链接时间与编译时间相比几乎可以忽略不计(除非您不断进行小的更改和构build,那么我希望您会感觉到它,我想这可能会经常发生)。