【问题标题】:Does using large libraries inherently make slower code?使用大型库是否会固有地使代码变慢?
【发布时间】:2011-01-15 21:21:19
【问题描述】:

我有一种心理抽搐,这让我不愿意在 C 和 C++ 等低级语言中使用大型库(如 GLibBoost)。在我看来,我认为:

嗯,这个图书馆有成千上万 投入了工时,而且一直 由知识渊博的人创造 关于语言比我以往任何时候都更重要。 他们的作者和粉丝说 库快速可靠, 而且功能看起来真的 有用,它肯定会阻止我 来自(严重)重新发明轮子。

但是该死的,我永远不会使用 该库中的每个函数。它是 太大了,可能会变得臃肿 这些年来;这是另一个球 并链接我的程序需要拖动。

Torvalds rant(尽管颇有争议)也让我不放心。

我的想法有什么根据,还是我只是不合理和/或无知?即使我只使用大型库的一两个功能,通过链接到该库是否会产生运行时性能开销?

我确信这也取决于具体的库是什么,但我通常想知道大型库是否会在技术层面上固有地引入低效率。

当我没有技术知识来判断我是否正确时,我已经厌倦了痴迷、喃喃自语和担心。

请让我摆脱痛苦!

【问题讨论】:

  • 这是其中之一“从技术上讲,答案是'不!',但是......等等......等等”,我建议你把它写成一个 CW 帖子。
  • 我不能代表 GLib,但将 Boost 视为 STL。你说“太大”;按照什么尺度?没有全球“太大”的规模。 STL 是不是太大了?你有什么依据说Boost会变得臃肿?为什么它甚至重要?听起来像是在寻找不存在问题的问题。
  • “它太大了,多年来它可能变得臃肿” - 很多臃肿是错误修复,你的新的小代码不会有。如果您不在乎结果是否正确,那么我为您准备了一个 O(1) 算法。
  • Torvalds 咆哮更多地与 C++ 与 C 相比,而不是 STL(可能被误用,但汇编也可能)太大。
  • 为什么要制作这个 CW?这是一个完全有效的问题,并且可以有好的和坏的答案。为什么一个好的答案不应该赢得作者代表?对这个问题的一个好的回答难道不是显示知识、乐于助人以及对 SO 的贡献吗?为答案没有价值的问题保留 CW,其中一个答案与另一个答案一样好(主要是民意调查问题或幽默主题)

标签: c++ c performance boost glib


【解决方案1】:

即使我只使用大型库的一两个功能,通过链接到该库是否会产生运行时性能开销?

一般来说不会。

如果所讨论的库没有很多与位置无关的代码,那么当动态链接器在请求时对库执行重定位时,就会产生启动成本。通常,这是程序启动的一部分。除此之外没有运行时性能影响。

链接器还擅长在构建时从静态链接库中删除“死代码”,因此您使用的任何静态库都将具有最小的大小开销。性能甚至不考虑它。

坦率地说,你担心的是错误的事情。

【讨论】:

  • 从您的应用程序调用共享库中与位置无关的代码时,仍然涉及到一定程度的间接性。但是,对于任何小型共享库都是如此。此外,一旦共享库被另一个应用程序分页到内存中,如果它使用相同的共享库,您的应用程序启动将会得到改善。
  • 对 Void 的小猪支持,在 Windows 中,DLL(或 Linux 中的共享库)的大小将决定加载时间,映射每个函数调用,除了调用本身。但是,Windows DLL 有一个简洁的功能,允许您指定内存中的首选偏移量。如果在首选偏移处成功加载,则不需要进行初始函数指针计算,从而消除该组计算。对于单个 DLL,这是无关紧要的。对于几十个或一百个(想想插件),这在启动时节省了相当多的时间。
  • 好点,你们俩。我的回答是从 Unix 系统的角度出发的,所以我很欣赏 DLL 的这一特性的提醒。
  • 此页面上有一些非常好的答案,但您非常简洁地解决了我的问题的核心并给出了坦率的意见。而且,您没有像其他人那样对 Boost 进行具体辩护(也许是我的错,因为我没有在我的问题中说清楚,但我对一般概念比对特定库更感兴趣)。所以,谢谢!
【解决方案2】:

我无法对 GLib 发表评论,但请记住,Boost 中的许多代码都是仅标头代码,并且考虑到用户只需为所使用的内容付费的 C++ 原则,这些库非常高效。有几个库需要您链接它们(想到正则表达式,文件系统),但它们是独立的库。使用 Boost,您不会链接到大型单体库,而只会链接到您使用的较小组件。

当然,另一个问题是 - 有什么替代方案?您想在需要时自己实现 Boost 中的功能吗?鉴于许多非常有能力的人已经在编写此代码并确保它可以跨多种编译器运行并且仍然高效,因此这可能不是一件简单的事情。另外,至少在一定程度上,您正在重新发明轮子。恕我直言,您可以更高效地度过这段时间。

【讨论】:

  • 学习使用 Boost 几乎等于编写所需(主要是非通用)代码所需的时间。但话虽如此,我必须提到 Boost 的知识很有价值(除非它们发生重大变化)
【解决方案3】:

Boost 不是一个大库。

它是许多小型图书馆的集合。它们中的大多数都非常小,它们包含在一两个标题中。使用boost::noncopyable 不会将boost::regexboost::thread 拖到您的代码中。它们是不同的库。它们只是作为同一个图书馆收藏的一部分分发。但是您只需为您使用的那些付费。

但总的来说,因为确实存在大型库,即使 Boost 不是其中之一:

我的想法有什么根据,还是我只是不合理和/或无知?即使我只使用大型库的一两个功能,通过链接到该库是否会产生运行时性能开销?

没有根据,或多或少。 你可以自己测试一下。

编写一个小的 C++ 程序并编译它。现在向它添加一个新函数,一个从未调用但已定义的函数。再次编译程序。假设启用了优化,它会被链接器剥离,因为它没有被使用。所以包含额外的未使用代码的成本为零。

当然也有例外。如果代码实例化了任何全局对象,它们可能不会被删除(这就是为什么包含 iostream 标头会增加可执行文件大小的原因),但通常,您可以包含任意数量的标头并链接到任意数量的库,并且它不会影响程序的大小、性能或内存使用*只要您不使用任何添加的代码。

另一个例外是,如果您动态链接到 .dll 或 .so,则必须分发整个库,因此不能删除未使用的代码。但是静态编译到可执行文件中的库(作为静态库(.lib 或 .a)或作为包含的头文件)通常可以由链接器修剪,删除未使用的符号。

【讨论】:

    【解决方案4】:

    代码性能的角度来看,大型库将:

    • 占用更多内存,如果它有运行时二进制文件(boost 的大部分部分不需要运行时二进制文件,它们是“仅标题”)。虽然操作系统只会将库中实际使用的部分加载到 RAM 中,但它仍然可以加载超出您需要的内容,因为加载内容的粒度等于页面大小(不过,在我的系统上只有 4 Kb)。
    • 需要更多时间来加载动态链接器,如果它再次需要运行时二进制文件。每次加载程序时,动态链接器都必须将您需要外部库包含的每个函数与其在内存中的实际地址相匹配。这需要一些时间,但只是一点点(但是,它在加载许多程序的规模上很重要,例如桌面环境的启动,但您别无选择)。

      是的,每次调用共享(动态链接)库的外部函数时,都会在运行时进行一次额外的跳转和几次指针调整

    开发人员的性能角度:

    • 添加外部依赖项。你会依赖别人。即使该库的免费软件,您也需要额外的费用来修改它。一些非常低级程序的开发人员(我指的是操作系统内核)讨厌依赖任何人——这是他们的专业技能。因此,咆哮。

      但是,这可以被视为一种好处。如果其他人习惯了boost,他们会在你的程序中找到熟悉的概念和术语,并且会更有效地理解和修改它。

    • 较大的库通常包含特定于库的概念,这些概念需要时间来理解。考虑 Qt。它包含信号和槽以及moc 相关的基础设施。与整个 Qt 的大小相比,学习它们只需要一小部分时间。但是如果你使用这么大的库的一小部分,那可能是个问题。

    【讨论】:

    • “因为加载的粒度很大——等于页面大小”。为什么那么大? 32 位系统上的典型页面大小为 4 KiB 字节。除非页面中有很多浪费的空间,否则我认为这不是问题。
    • “需要更多时间来加载”。如果其他程序使用相同的共享库并且这些程序正在运行,那么该库很可能已经被分页到内存中。
    • +1 对于开发人员的性能角度 - 虽然我只是要求技术角度,但您关于需要花时间理解特定于库的概念的观点是一个很好的观点,也是我的另一个原因我推迟编写大量投资于外部库的代码。
    • @Void,除非 DLL 加载到首选位置,否则加载时间将部分包括重新计算函数的应用程序地址,即使 DLL 已经加载。至于 linux 和共享库,我想会发生类似的事情。所以一般来说更多的入口点=更多的加载时间,除非加载到首选地址。
    • @Gavin,我会得出另一个结论。如果我的代码大量使用大型库,那么学习新概念可能是一项有益的投资。
    【解决方案5】:

    多余的代码不会神奇地使处理器运行速度变慢。它所做的只是坐在那里占用一点内存。

    如果您是静态链接并且您的链接器完全合理,那么它只会包含您实际使用的函数。

    【讨论】:

    • 但是话又说回来,大型库通常建立在具有许多层等的更复杂的对象模型之上,这会使其变慢。但是等等,通过花哨的分层消除了一些复杂性,使算法得到了更好的深思熟虑,因此结果实际上更快......是的,但是......
    • @mjv,但这不是问题所在。问题是库的大小是否会产生影响,而不是该库中代码的复杂性。
    • @Glen, all:是的,在这一点上,Anon 和其他答案在技术上是正确的:本质上 不会让大型库变慢。我们都应该感谢这一点洞察力。
    • 将您使用的代码部分在内存中分开会影响分页和缓存一致性,在适当的条件下,这两者都会对性能产生巨大影响。
    【解决方案6】:

    我喜欢框架、库集和某些类型的开发工具的术语是平台技术。平台技术的成本超出了对代码大小和性能的影响。

    1. 如果您的项目本身打算用作库或框架,您最终可能会将您的平台技术选择推给使用您的库的开发人员。

    2. 如果您以源代码形式分发项目,您最终可能会将平台技术选择推给最终用户。

    3. 如果您不静态链接所有选择的框架和库,最终可能会给最终用户带来库版本控制问题。

    4. 编译时间会影响开发人员的工作效率。增量链接、预编译头、适当的头依赖管理等可以帮助管理编译时间,但不能消除与某些平台技术引入的大量内联代码相关的编译器性能问题。

    5. 对于作为源代码分发的项目,编译时间会影响项目的最终用户。

    6. 许多平台技术都有自己的开发环境要求。这些要求可能会累积起来,使项目的新开发人员难以复制允许编译和调试所需的环境。

    7. 使用某些平台技术实际上为项目创建了一种新的编程语言。这让新开发者更难做出贡献。

    所有项目都具有平台技术依赖性,但对于许多项目而言,将这些依赖性保持在最低限度确实有好处。

    【讨论】:

    • 即使你静态地链接你的依赖,你可能仍然会给最终用户带来版本控制问题。除非您明确隐藏符号,否则它们将在您生产的任何最终产品中可见,无论是共享库还是静态库。
    • +1。我很惊讶这没有更早出现,因为 &%¤#¤% 正确编译各种 C++ 库并随着时间的推移保持不同版本之间的兼容性可以做很多工作。
    【解决方案7】:

    如果它们是动态链接的,则加载这些库时可能会产生少量开销。这通常只占程序运行时间的一小部分。

    但是,一旦所有内容都加载完毕,就不会有任何开销。

    如果你不想使用所有的提升,那就不要。它是模块化的,因此您可以使用您想要的部分而忽略其余部分。

    【讨论】:

    • +1:在现代操作系统中,未使用的代码页将被换出,不再被引用。
    • 事实上,很多现代操作系统可能一开始就不会被换掉。
    • 我相信 KDE 项目在优化动态链接应用程序的加载时间方面付出了巨大的努力。这是因为 KDE 是一个大型 C++ 系统,而且 C++ 名称比 C 名称大得多。因为运行时链接器执行大量散列和散列表搜索,所以这会产生很大的不同。例如,我看到dlopen() 显示为在分析器中运行的短期程序的主要成本。有关详细信息,请参阅lwn.net/Articles/192624。但是...这仅适用于动态链接库。
    【解决方案8】:

    更大并不意味着更慢。与其他一些答案相反,完全存储在头文件中的库和存储在目标文件中的库之间也没有固有的区别。

    仅标题库可以具有间接优势。大多数基于模板的库必须是仅头文件(或者很多代码最终都在头文件中),并且模板确实提供了很多优化机会。在典型的目标文件库中获取代码并将其全部移动到标题中不会,但是,通常会有很多好的效果(并且可能导致代码膨胀)。

    特定库的真正答案通常取决于其整体结构。很容易将“Boost”视为巨大的东西。事实上,它是一个庞大的库集合,其中大部分都非常小。你不能对整个 Boost 说太多(有意义的),因为各个库是由不同的人编写的,具有不同的技术、目标等。其中一些(例如 Format、Assign)确实比几乎任何东西都慢你很有可能自己做。其他人(例如 Pool)提供了您可以自己做但可能不会做的事情,以至少获得较小的速度改进。少数人(例如 uBlas)使用重型模板魔法来运行速度比任何人都快,但我们中的一小部分人希望自己实现。

    当然,有相当多的库确实是独立的大型库。在很多情况下,这些确实比您自己编写的要慢。特别是,它们中的许多(大多数?)试图比您自己编写的几乎任何东西都更通用。虽然这并不必然导致代码变慢,但在这个方向上肯定有强烈的趋势。与许多其他代码一样,当您在商业上开发库时,客户往往对功能更感兴趣,而不是速度大小之类的东西。

    一些库还投入大量空间、代码(通常至少是一点时间)来解决您可能根本不关心的问题。例如,几年前我使用了一个图像处理库。它对 200 多种图像格式的支持听起来确实令人印象深刻(并且在某种程度上确实如此),但我很确定我从未使用它来处理超过十几种格式(而且我可能只支持其中的一半)许多)。 OTOH,尽管如此,它仍然非常快。支持较少的市场可能会限制他们的市场,以至于代码实际上会更慢(例如,它处理 JPEG 比 IJG 更快)。

    【讨论】:

      【解决方案9】:

      正如其他人所说,添加动态库时会有一些开销。首次加载库时,必须执行重定位,尽管如果正确编译库,这应该是一个很小的成本。由于需要搜索的库数量增加,查找单个符号的成本也增加了。

      添加另一个动态库的内存成本很大程度上取决于您实际使用了多少。一页代码在执行之前不会从磁盘加载。但是会加载库文件中内置的标头、符号表和哈希表等其他数据,这些数据一般与库的大小成正比。

      glibc 的主要贡献者 Ulrich Drepper 有一个 great document,它描述了动态库的过程和开销。

      【讨论】:

      • 我喜欢用 Big-O 表示法来解释相对成本。 O(R + rn * log(s))... R = # rel 位置; r=命名重定位; n=参与 DSO 的数量(共享对象); s = 要解析的符号数。并且得出的结论是,减少符号(导出和导入)的数量是最小化特定性能影响的关键。
      【解决方案10】:

      取决于链接器的工作方式。一些链接器是惰性的,会包含库中的所有代码。更高效的链接器只会从库中提取所需的代码。我对这两种类型都有经验。

      对于任何一种类型的链接器,较小的库都不会那么担心。小型库的最坏情况是少量未使用的代码。许多小型库可能会增加构建时间。权衡将是构建时间与代码空间。

      链接器的一个有趣测试是经典的Hello World程序:

      #include <stdio>
      #include <stdlib>
      int main(void)
      {
        printf("Hello World\n");
        return EXIT_SUCCESS;
      }
      

      printf 函数有很多依赖关系,因为它可能需要所有格式。一个懒惰但快速的链接器可能包含一个“标准库”来解析所有符号。更高效的库将仅包含 printf 及其依赖项。这会使链接器变慢。

      上面的程序可以用puts与这个程序进行比较:

      #include <stdio>
      #include <stdlib>
      int main(void)
      {
        puts("Hello World\n");
        return EXIT_SUCCESS;
      }
      

      一般情况下,puts 版本应该小于printf 版本,因为puts 没有格式化需求,因此依赖关系更少。惰性链接器将生成与printf 程序相同的代码大小。

      总之,库大小决定对链接器有更多的依赖性。具体来说,链接器的效率。如有疑问,许多小型库将较少依赖链接器的效率,但会使构建过程更加复杂和缓慢。

      【讨论】:

        【解决方案11】:
        1. 一般来说,与性能问题有关的事情不是取悦他们,因为这样做是为了猜测他们是一个问题,因为如果你不知道他们是,你在猜测,猜测是“过早优化”背后的核心概念。与性能问题有关的事情是,当你遇到它们时,而不是之前,诊断它们。这些问题几乎从来都不是你能猜到的。 Here's an extended example.

        2. 如果你做了大量的工作,你就会认识到往往会导致性能问题的设计方法,无论是在你的代码中还是在库中。 (库肯定会出现性能问题。)当您了解这一点并将其应用到项目中时,在某种意义上您过早地进行了优化,但无论如何它都有避免问题的预期效果。如果我可以总结一下你可能会学到的东西,太多的抽象层和过分夸大的类层次结构(尤其是那些充满通知式更新的)往往是导致性能问题的原因。

          李>

        同时,我也分享您对 3rd-party 库等的谨慎看法。太多次我参与过一些项目,其中一些 3rd 方包被“利用”以实现“协同效应”,然后供应商要么烟消云散,要么放弃产品,要么因为微软改变了操作系统而让它过时。然后,我们严重依赖 3rd-party 包的产品开始无法正常工作,需要我们投入大量资金,而原来的程序员早已不在。

        【讨论】:

        • 是的!仅仅因为其他人做了这项工作,这并不意味着(1)他们值得信赖,(2)他们做对了或者他们愿意做对,或者(3)他们会在 2系统仍然依赖于他们的年代。不过,如果我必须重新发明轮子来避免这些问题,那么从开源项目开始肯定比从头开始更好。
        • @Tom:同意。我认为这是开源概念的主要好处之一——我们应该为此付出代价:-)
        【解决方案12】:

        “另一个球和链子”。真的吗?

        或者它是一个稳定、可靠的平台,可以让您的应用程序首先得到支持?

        考虑到有些人可能喜欢“太大而且……臃肿”的库,因为他们将它用于其他项目并且非常信任它。

        确实,他们可能拒绝弄乱您的软件,特别是因为您避免使用明显的“太大而且……臃肿”的库。

        【讨论】:

          【解决方案13】:

          从技术上讲,答案是肯定的。然而,这些低效率是非常实际上很少重要。我将在这里假设一种静态编译的语言,如 C、C++ 或 D。

          在现代操作系统上将可执行文件加载到内存中时,地址空间只是简单地映射到它。这意味着,无论可执行文件有多大,如果有整个页面大小的代码块未被使用,它们将永远不会触及物理内存。不过,您会浪费地址空间,而且有时这在 32 位系统上可能会有些影响。

          当您链接到一个库时,一个好的链接器通常会丢弃您不使用的多余内容,尽管尤其是在模板实例化的情况下,这并不总是会发生。因此,您的二进制文件可能比严格必要的要大一些。

          如果您将不使用的代码与您确实使用的代码大量交错使用,则最终可能会浪费 CPU 缓存中的空间。但是,由于缓存行很小(通常为 64 字节),因此很少会在实际重要的程度上发生这种情况。

          【讨论】:

            【解决方案14】:

            问问自己你的目标是什么。它是今天的中端工作站吗?没问题。是较旧的硬件还是有限的嵌入式系统,那么它可能是。

            正如之前的发帖人所说,仅在其中保存代码不会降低性能(它可能会减少缓存的局部性并增加加载时间)。

            【讨论】:

              【解决方案15】:

              fwiw,我在 Microsoft Windows 和我们构建 Windows 时工作;为 SIZE 编译的构建比为 SPEED 编译的构建更快,因为您需要更少的页面错误命中。

              【讨论】:

                【解决方案16】:

                FFTW 和 ATLAS 是两个相当大的库。奇怪的是,它们在世界上最快的软件中扮演着重要角色,这些应用程序经过优化以在超级计算机上运行。不,使用大型库不会使您的代码变慢,尤其是当您自己实现 FFT 或 BLAS 例程时。

                【讨论】:

                  【解决方案17】:

                  您的担心是对的,尤其是在提升方面。这不是因为写它们的人不称职,而是由于两个问题。

                  1. 模板只是天生臃肿的代码。这在 10 年前并不重要,但现在 CPU 比内存访问快得多,而且这种趋势仍在继续。我几乎可以说模板是一个过时的功能。

                  对于通常有点实用的用户代码来说还不错,但在许多库中,所有内容都是根据其他模板或多个项目上的模板定义的(意味着指数模板代码爆炸)。

                  只需添加 iostream 即可为您的代码增加大约 3 mb (!!!)。现在添加一些 boost 废话,如果你简单地声明几个特别奇怪的数据结构,你就有 30 mb 的代码。

                  更糟糕的是,您甚至无法轻松地对此进行分析。我可以告诉你我编写的代码和模板库中的代码之间的区别是巨大的,但是对于更天真的方法,你可能认为你从简单的测试中做得更糟,但代码膨胀的成本会在一个大的现实世界中使用它的工具应用程序。

                  1. 复杂性。当您查看 Boost 中的内容时,它们都会在很大程度上使您的代码复杂化。诸如智能指针、仿函数之类的东西,各种复杂的东西。现在,我不会说使用这些东西从来都不是一个好主意,但几乎所有这些都需要某种高昂的成本。特别是如果您不完全理解,我的意思是确切地说,它在做什么。

                  但人们对它赞不绝口,并假装它与“设计”有关,因此人们认为这是你应该做所有事情的方式,而不仅仅是一些应该很少使用的极其专业的工具。如果有的话。

                  【讨论】:

                  • 模板并非“天生臃肿”。 vector&lt;int&gt; 的代码大小与它不是模板并且为存储整数而被硬编码时完全相同。如果您为许多不同类型实例化模板,就会出现膨胀(但即便如此,现代编译器也很擅长消除通用代码)我不知道您是如何组成这些数字的,但我无法重现它们.将 iostream 标头添加到我的代码中最多会增加几百 KB,具体取决于编译器,我喜欢看看哪个 boost lib 可以为您提供 30mb 的可执行文件。
                  • 我向您询问了示例。您说包含iostream 会为您的可执行文件添加3MB 代码。我问你如何重现它。你说添加对一些未命名的 Boost 库的依赖会为你的可执行文件增加 30MB,我问是哪个库。而你无法回答,只能说我无知。不,我认为您的反对票是完全合理的。在这里,我也会免费将您的评论标记为冒犯性的。
                  • 我只是在看一些包含iostream的C++代码;它编译为 16KB。这有点小于 3MB ;-)
                  • @trolle3000:不,不是。显然你也不知道 C++。在 C++ 中,16KB 正好是 3MB ;)
                  • 哈哈。 “再一次,我得到了最好的答案,但却是最受贬低的。具有讽刺意味的是。”我笑得很开心。你为什么不成熟起来,意识到自己并不像你想象的那么聪明或特别,或者离开? jalf 对编程一无所知?是的。 “我取得的成就远远超过大多数程序员的梦想。”大声笑,虽然我有时很自大。
                  猜你喜欢
                  • 2013-01-12
                  • 1970-01-01
                  • 2023-01-14
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多