使用大型库固有地使较慢的代码?
我有一个心理抽象,这使得我不愿意在C和C ++等低级语言中使用大型库(如GLib或Boost )。 在我看来,我想:
那么这个图书馆就有上千个人工小时,而且这个图书馆是由那些比以往任何时候都更了解这门语言的人创造出来的。 他们的作者和粉丝们说,图书馆是快速可靠的,function看起来非常有用,它肯定会阻止我(重)重新发明轮子。
但是,该死的,我永远不会使用该库中的每个function。 它太大了,多年来可能会变得臃肿; 这是我的计划需要拖延的另一个球链。
Torvalds的咆哮 (虽然有争议)并不完全让我放心。
我的思想有没有基础,还是我只是不合理和/或无知? 即使我只使用一个或两个大型库的function,通过链接到该库我会承担运行时性能开销?
我相信这也取决于具体的图书馆是什么,但是我一般都很想知道大型图书馆是否会在技术层面上内在地引入低效率。
当我没有技术知识知道自己是否正确的时候,我厌倦了这种痴迷和担心。
请把我从痛苦中解救出来!
即使我只使用一个或两个大型库的function,通过链接到该库我会承担运行时性能开销?
一般来说,没有。
如果有问题的库没有很多与位置无关的代码,那么将会有启动成本,而dynamic链接器在请求时会对库执行重定位。 通常情况下,这是该计划启动的一部分。 除此之外,没有运行时间的性能影响。
链接器也擅长在构build时从静态链接库中删除“死代码”,因此您使用的任何静态库都将具有最小的开销。 性能甚至没有进入它。
坦率地说,你担心错误的事情。
我不能评论GLib,但请记住,Boost中的很多代码是仅用于头文件的,并且由于用户的C ++原则只支付他们使用的内容,因此这些库非常高效。 有几个库,需要你链接到他们(正则expression式,文件系统等想到),但他们是单独的库。 借助Boost,您不会链接到一个庞大的整体库,而只能针对您使用的较小组件。
当然,另一个问题是 – 替代scheme是什么? 你是否想要实现在需要的时候提升自己的function? 考虑到很多非常有能力的人都在编写这个代码,并确保它能够跨越多个编译器工作,而且仍然是高效的,这可能不是一个简单的任务。 另外,你至less在一定程度上重新发明了车轮。 恕我直言,你可以花更多的时间更有成效。
Boost不是一个大的图书馆。
这是许多小型图书馆的集合。 他们中的大多数都很小,他们被包含在一个或两个头。 使用boost::noncopyable
不会将boost::regex
或boost::thread
拖到代码中。 他们是不同的图书馆。 它们只是作为同一个图书馆collections的一部分分发。 但是,你只支付你使用的那些。
但一般来说,由于大型图书馆确实存在,即使Boost不是其中之一:
我的思想有没有基础,还是我只是不合理和/或无知? 即使我只使用一个或两个大型库的function,通过链接到该库我会承担运行时性能开销?
没有根据, 或多或less 。 你可以自己testing一下。
编写一个小的C ++程序并编译它。 现在给它添加一个新的函数,一个永远不会被调用,但是被定义的函数。 再次编译程序。 假设优化已启用,链接器会将其删除,因为它未被使用。 所以包含额外的未使用代码的代价是零。
当然,也有例外。 如果代码实例化任何全局对象,那些可能不会被删除(这就是为什么包括iostream
头增加了可执行文件的大小),但一般来说,你可以包括尽可能多的头和连接到尽可能多的库,只要你喜欢,不影响程序的大小,性能或内存使用情况*只要不使用任何添加的代码即可。
另一个例外是,如果您dynamic链接到.dll或.so,则整个库必须分发,因此不能剥离未使用的代码。 但静态编译到您的可执行文件(静态库(.lib或.a)或仅包含头文件)的库通常可以通过链接器进行修剪,删除未使用的符号。
从代码性能的angular度来看,大型图书馆将会:
- 占用更多的内存 ,如果它有一个运行时二进制文件(大部分的
boost
不需要运行时的二进制文件,它们是“仅标题”)。 虽然操作系统只会将内存中实际使用的部分加载到内存中,但由于加载内容的粒度等于页面大小(仅在我的系统上,只有4Kb),所以仍然可以加载超过您需要的内存。 -
如果再次需要运行时的二进制文件,需要花费更多的时间来加载dynamic连接器。 每次加载程序时,dynamic链接器都必须将每个需要外部库包含的函数与其在内存中的实际地址进行匹配。 这需要一些时间,但只是一点点(但是,在加载许多程序的规模,如桌面环境的启动,但你没有在那里select)。
是的,每次调用共享 (dynamic链接)库的外部函数时,运行时都需要额外的跳转和几个指针调整
从开发人员的性能angular度来看:
-
添加一个外部的依赖 。 你将依靠别人 。 即使该图书馆的免费软件,你也需要额外的费用来修改它。 Veery低级程序(我在谈论OS内核)的一些开发人员不喜欢依赖任何人 – 这是他们的专业特权。 因此,咆哮。
但是,这可以被认为是一种好处。 如果其他人习惯了
boost
,他们会在程序中find熟悉的概念和术语,并且会更有效地理解和修改它。 -
更大的库通常包含特定于库的概念 , 这些概念需要时间才能理解。 考虑Qt。 它包含信号和插槽以及与
moc
相关的基础设施。 与整个Qt的大小相比,学习它们只需要很短的时间。 但是,如果你使用这样一个大型图书馆的一小部分,这可能是一个问题。
多余的代码不会神奇地使处理器运行速度变慢。 它所做的只是坐在那里占据一点点的记忆。
如果你是静态链接,并且链接器是合理的,那么它将只包含你实际使用的函数。
我喜欢的框架,库集和某些types的开发工具这个术语是平台技术。 平台技术的成本已经超出了对代码大小和性能的影响。
-
如果您的项目本身是用作库或框架的,那么您最终可能会将开发人员的平台技术select推向使用您的库的开发人员。
-
如果您以源代码forms分发项目,则最终可能会推动最终用户的平台技术select。
-
如果您没有静态链接所有选定的框架和库,则最终可能会导致最终用户因库版本问题而受到影响。
-
编译时间影响开发人员的效率。 增量链接,预编译头文件,正确的头文件依赖pipe理等可以帮助pipe理编译时间,但并不能消除某些平台技术引入的大量内联代码所带来的编译器性能问题。
-
对于以源代码分发的项目,编译时间会影响项目的最终用户。
-
许多平台技术都有自己的开发环境要求。 这些要求可能会累积起来,使得项目上的新开发人员能够复制允许编译和debugging所需的环境变得困难和耗时。
-
实际上使用一些平台技术为项目创build了一种新的编程语言。 这使新开发人员难以贡献。
所有项目都具有平台技术依赖性,但对于许多项目来说,将这些依赖关系保持在最低限度是有好处的。
加载这些库时,如果它们是dynamic链接的,可能会有小的开销。 这通常只是程序运行时间的一小部分。
但是一旦所有的东西都加载完毕,就不会有开销。
如果你不想使用所有的提升,那么不要。 它是模块化的,所以你可以使用你想要的部分,而忽略其余部分。
更大的本质上并不意味着更慢。 与其他一些答案相反,完全存储在头文件中的库和存储在目标文件中的库之间没有内在的区别。
只有标题的库可以具有间接优势。 大多数基于模板的库必须是仅包含头的(或者大量的代码以头文件forms出现),模板确实为优化提供了很多机会。 在一个典型的对象文件库中取代码并将其全部移到头文件中通常不会有很多好的效果(并可能导致代码膨胀)。
特定图书馆的真正答案通常取决于其整体结构。 很容易把“Boost”看成是一个巨大的东西。 实际上,这是一个庞大的图书馆藏书,其中大部分都很小。 关于Boost作为一个整体,你不能说太多(有意义的),因为单独的库是由不同的人编写的,具有不同的技术,目标等。其中一些(例如Format,Assign)实际上比几乎任何东西都慢你很可能会自己做。 其他人(比如Pool)提供了你自己可以做的事情,但是可能不会,至less得到微小的速度提升。 less数(例如uBlas)使用重型模板魔法运行速度超过任何一个,但只有很小一部分人可以希望我们自己实现。
当然,相当多的图书馆确实是个人大型的图书馆。 在很多情况下,这些比你自己写的要慢。 特别是,他们中的很多(大多数)试图比几乎任何你可能自己写的任何东西都更普遍。 虽然这不一定会导致代码变慢,但朝这个方向肯定有一个强烈的倾向。 与许多其他代码一样,当您在商业开发图书馆时,客户往往比速度大小等对function更感兴趣。
一些图书馆也投入了大量的空间,代码(通常至less有点时间)来解决你可能根本不在乎的问题。 例如,几年前我使用了一个image processing库。 它对200多种图像格式的支持听起来非常令人印象深刻(而且从某种意义上来说),但是我确信我从来没有用过它来处理十多种格式(而且我可能只能支持其中的一半许多)。 OTOH,即使这一切仍然非常快。 支持更less的市场可能会限制它们的市场,以至于代码实际上会更慢(例如,它比IJG更快地处理JPEG)。
正如其他人所说,添加dynamic库时有一些开销。 首次加载库时,必须执行重定位,但是如果库编译正确,这应该是一个小的成本。 查找单个符号的成本也增加了,因为需要search的库的数量增加了。
添加另一个dynamic库的内存成本在很大程度上取决于您实际使用的数量。 一段代码将不会从磁盘加载,直到它的执行。 但是,库文件中内置的其他数据(如标题,符号表和哈希表)将被加载,这些数据通常与库的大小成正比。
glibc的主要贡献者Ulrich Drepper提供了一个很好的文档 ,描述了dynamic库的过程和开销。
取决于链接器的工作方式。 一些连接器是懒惰的,将包括库中的所有代码。 效率更高的链接器只会从库中提取所需的代码。 我有两种types的经验。
较小的图书馆将不用担心任何一种链接器。 小图书馆的最坏情况是less量未使用的代码。 许多小型图书馆可能会增加构build时间。 这个折衷将是代码空间的构build时间。
链接器的一个有趣的testing是经典的Hello World程序:
#include <stdio> #include <stdlib> int main(void) { printf("Hello World\n"); return EXIT_SUCCESS; }
printf
函数由于可能需要的所有格式而具有很多依赖关系。 一个懒惰但快速的链接器可能包含一个“标准库”来parsing所有的符号。 一个更高效的库将只包含printf
及其依赖项。 这使得链接器变慢。
上面的程序可以用puts
比较这个:
#include <stdio> #include <stdlib> int main(void) { puts("Hello World\n"); return EXIT_SUCCESS; }
一般来说, puts
版本应该小于printf
版本,因为puts
没有格式化需要,所以依赖性较小。 懒链接器将生成与printf
程序相同的代码大小。
总之,库大小的决定有更多的依赖链接器。 具体来说,连接器的效率。 如果有疑问,许多小型图书馆将更less地依赖链接器的效率,但是使构build过程更复杂和更慢。
-
一般来说,与performance有关的事情并不是为了娱乐他们,因为这样做是为了猜测他们是一个问题,因为如果你不知道他们是谁 ,你猜测,猜测是中心“不成熟优化”背后的概念。 与性能问题有关的事情是,当你有, 而不是之前 ,诊断它们。 这些问题几乎从来不会是你猜到的。 这是一个扩展的例子。
-
如果你这么做的话,你会认识到那些会导致性能问题的devise方法,无论是在你的代码中还是在库中。 (图书馆当然可能有性能问题。)当你学习并将其应用到项目中时,从某种意义上说,你正在过早地进行优化,但无论如何,它都有避免问题的预期效果。 如果我能总结一下你可能会学到的东西,那就是太多的抽象层次和夸大的类层次结构(特别是那些充满通知式更新的层次结构)常常是性能问题的原因。
同时,我分享你对第三方图书馆等的眷恋。 我曾经在一些项目中工作过,有些第三方软件包被“杠杆化”为“协同”,然后供应商不是冒烟就是放弃了产品,或者因为微软改变了操作系统中的东西而过时了。 那么,我们依靠第三方软件包的产品就开始不能工作了,而原来的程序员早就离开了,这就需要我们付出很大的代价。
“另一个球和链”。 真?
还是它是一个稳定,可靠的平台,使您的应用程序在第一位?
想想看,有些人可能会喜欢一个“太大,臃肿”的图书馆,因为他们把它用于其他项目,并真的相信它。
事实上,他们可能会拒绝专门用你的软件,因为你避免使用明显的“太大,臃肿”的图书馆。
技术上来说,答案是肯定的。 但是,这些低效率实际上非常重要。 我将在这里假设一个静态编译的语言,如C,C ++或D。
当一个可执行文件被加载到现代操作系统的内存中时,地址空间被简单地映射到它。 这意味着,无论可执行文件有多大,如果有整个页面大小的代码块没有使用,它们将永远不会触及物理内存。 但是,你将浪费地址空间,偶尔在32位系统上会有一些问题。
当你链接到一个库时,一个好的链接器通常会抛出你不使用的多余的东西,尽pipe尤其是在模板实例化的情况下,这并不总是发生。 因此,你的二进制文件可能比绝对必要的大一点。
如果您的代码不是与您使用的代码大量交叉使用,那么最终可能会浪费CPUcaching中的空间。 但是,由于caching行很小(通常是64字节),所以很less发生实际的重要程度。
问自己你的目标是什么。 这是今天的中端工作站 – 没问题。 是旧的硬件还是有限的embedded式系统,那么它可能是。
正如之前的海报所说的那样,只要有代码就不会在性能上花费太多(这可能会减lesscaching的位置并增加加载时间)。
fwiw,我在微软Windows上工作,当我们build立Windows; 为SIZE编译的编译速度比为SPEED编译的编译速度快,因为您的页面错误命中次数较less。
FFTW和ATLAS是两个相当大的库。 奇怪的是,他们在世界上运行速度最快的软件中扮演着重要angular色,优化应用程序以运行在超级计算机上。 不,使用大型库不会使代码变慢,尤其是当替代方法为自己实现FFT或BLAS例程时。
你很担心,特别是在提振的时候。 这不是因为任何人写作无能,而是由于两个问题。
- 模板本身就是臃肿的代码。 10年前这一点并不重要,但是现在的CPU比内存访问要快得多,这个趋势还在继续。 我几乎说模板是一个过时的function。
用户代码通常不太实用,但是在许多库中,所有东西都是根据多个项目上的其他模板或模板(即指数模板代码爆炸)来定义的。
只需在iostream中添加大约3 MB(!!!)到您的代码。 现在添加一些提高废话,如果你sinply声明几个特别奇怪的数据结构,你有30 MB的代码。
更糟糕的是,你甚至不能轻易分析这一点。 我可以告诉你,我写的代码与模板库中的代码之间的区别是DRAMATIC,但是对于一个更加朴素的方法,你可能会认为你从简单的testing中变得更糟糕,但是代码膨胀的代价将会在一个大的现实世界应用程序。
- 复杂。 当你看看Boost中的东西时,它们都是使你的代码在很大程度上变得复杂的东西。 像智能指针,函子,各种复杂的东西。 现在,我不会说使用这个东西不是一个好主意,但几乎所有的东西都有很大的成本。 特别是如果你不明白,我的意思是,它在做什么。
但人们对此大加赞赏,并假装它与“devise”有关,所以人们会觉得这是你应该尽一切努力的方式,而不是一些应该很less使用的极其专业化的工具。 如果曾经。