静态链接与动态链接

Posted

技术标签:

【中文标题】静态链接与动态链接【英文标题】:Static linking vs dynamic linking 【发布时间】:2010-12-31 21:24:57 【问题描述】:

在某些情况下,是否有任何令人信服的性能理由来选择静态链接而不是动态链接,反之亦然?我听过或读过以下内容,但我对这个主题的了解还不够,无法保证其真实性。

1) 静态链接和动态链接在运行时性能上的差异通常可以忽略不计。

2) (1) 如果使用使用配置文件数据来优化程序热路径的分析编译器则不成立,因为通过静态链接,编译器可以同时优化您的代码和库代码。使用动态链接只能优化您的代码。如果大部分时间都花在运行库代码上,这会产生很大的不同。否则,(1) 仍然适用。

【问题讨论】:

"使用静态链接,编译器可以优化 .. 库代码",但前提是它也可以编译!如果你只是链接到预编译的目标文件,你的编译器就没有机会优化它们。 如果这是真的,那么您是对的,但是对于现代编译器的真实性存在一些疑问,如果有人可以以一种或另一种方式验证这一点,那就太好了。跨度> 使用编译为本机代码的编译器(像大多数 C/C++ 编译器一样)没有进一步优化代码的机会。如果代码被编译为某种中间语言(如 .Net IL),则在加载库时调用 JIT 编译器以将其编译为本机代码。随着 JIT 编译器的发展,最终编译会随着时间的推移变得越来越好。 @Eloff:VS2008 在启用 LTCG 的情况下完全可以做到这一点。 (不过,lib 文件变得很大。。)我玩弄了它,对于对“我的编译器能为我做什么”感兴趣的人来说,这简直是惊人的。 【参考方案1】: 动态链接可以减少总资源消耗(如果多个进程共享同一个库(当然包括“相同”中的版本))。我相信这是推动它在大多数环境中存在的论点。这里的“资源”包括磁盘空间、RAM 和缓存空间。当然,如果您的动态链接器不够灵活,则存在DLL hell 的风险。 动态链接意味着错误修复和库升级传播以改进您的产品,而无需您交付任何东西。 插件总是需要动态链接。 静态链接,意味着您可以知道代码将在非常有限的环境中运行(在启动过程的早期,或在救援模式下)。 静态链接可以使二进制文件更容易分发到不同的用户环境(以发送更大、更消耗资源的程序为代价)。 静态链接可能允许稍微更快的启动时间,但这在某种程度上取决于您的程序的大小和复杂性以及操作系统的加载策略。

进行一些编辑,以在 cmets 和其他答案中包含非常相关的建议。我想指出,你打破这个的方式很大程度上取决于你计划在什么环境中运行。最小的嵌入式系统可能没有足够的资源来支持动态链接。稍微大一点的小系统可能很好地支持动态链接,因为它们的内存足够小,使得动态链接节省的 RAM 非常有吸引力。成熟的消费类 PC 像 Mark notes 一样拥有巨大的资源,您或许可以让便利性问题推动您对此事的思考。


解决性能和效率问题:视情况而定

传统上,动态库需要某种粘合层,这通常意味着函数寻址中的双重调度或额外的间接层,并且可能会花费一点速度(但函数调用时间实际上是运行时间的很大一部分吗?? ?)。

但是,如果您运行的多个进程都大量调用同一个库,那么在使用动态链接时,相对于使用静态链接,您最终可以节省缓存行(从而赢得运行性能)。 (除非现代操作系统足够聪明,可以注意到静态链接二进制文件中的相同段。似乎很难,有人知道吗?)

另一个问题:加载时间。您在某个时候支付装载费用。何时支付此费用取决于操作系统的工作方式以及您使用的链接。也许你宁愿推迟支付,直到你知道你需要它。

请注意,静态与动态链接传统上不是优化问题,因为它们都涉及单独编译到目标文件。但是,这不是必需的:编译器原则上可以将“静态库”最初“编译”为摘要的 AST 形式,然后通过将这些 AST 添加到为主代码生成的 AST 中来“链接”它们,从而实现全局优化。我使用的系统都没有这样做,所以我无法评论它的效果。

回答性能问题的方法是始终通过测试(并尽可能使用类似于部署环境的测试环境)。

【讨论】:

资源消耗基本上是代码空间,随着时间的推移,这个问题越来越少。如果 500K 的库在 5 个进程之间共享,则节省了 2MB,不到 3GB RAM 的 0.1%。 如果库也共享相同的虚拟映射(所有进程中相同的物理和虚拟地址),那么动态链接不也可以在处理器的 MMU 中保存 TLB 插槽吗? 还有一个动态链接可以很容易地用更好的版本更新有缺陷的库代码。 @Zan 它还可以轻松地将错误代码添加到工作版本中。 "插件总是需要动态链接。"这是不正确的。一些插件模型(例如 Apple 的 AudioUnits)可以在单独的进程中运行插件并使用 IPC。这是插件动态链接的更安全替代方案(插件不会使主机崩溃)。建议将答案更新为“插件可能需要动态链接”或类似内容。【参考方案2】:

1) 基于调用 DLL 函数总是使用额外的间接跳转这一事实。今天,这通常可以忽略不计。在 DLL 内部,i386 CPU 的开销更大,因为它们无法生成与位置无关的代码。在 amd64 上,跳转可以相对于程序计数器,所以这是一个巨大的改进。

2) 这是正确的。通过分析引导的优化,您通常可以获得大约 10-15% 的性能。现在 CPU 速度已达到极限,可能值得这样做。

我要补充一点:(3) 链接器可以将函数安排在缓存效率更高的分组中,从而将代价高昂的缓存级别未命中率降至最低。它还可能特别影响应用程序的启动时间(基于我使用 Sun C++ 编译器看到的结果)

并且不要忘记,使用 DLL 不能执行死代码消除。根据语言的不同,DLL 代码也可能不是最佳的。虚函数总是虚函数,因为编译器不知道客户端是否覆盖了它。

由于这些原因,如果没有真正需要 DLL,则只需使用静态编译。

编辑(回答评论,用户下划线)

这是一个关于位置无关代码问题的好资源http://eli.thegreenplace.net/2011/11/03/position-independent-code-pic-in-shared-libraries/

正如解释的那样,x86 没有 AFAIK 用于 15 位跳转范围之外的任何其他内容,而不是用于无条件跳转和调用。这就是为什么具有超过 32K 的函数(来自生成器)一直是个问题,需要嵌入式蹦床。

但在流行的 x86 操作系统(如 Linux)上,您无需关心 .so/DLL 文件是否不是使用gcc 开关-fpic(强制使用间接跳转表)生成的。因为如果你不这样做,代码就像一个普通的链接器会重新定位它一样被固定。但是在这样做的同时,它使代码段不可共享,并且需要将代码从磁盘完整映射到内存并在它可以使用之前全部接触它(清空大部分缓存,命中 TLB)等等。有一段时间当这被认为很慢时。

所以你将不再有任何好处。

我不记得是什么操作系统(Solaris 或 FreeBSD)给我的 Unix 构建系统带来了问题,因为我只是没有这样做,并且想知道它为什么会崩溃,直到我将 -fPIC 应用到 gcc

【讨论】:

我喜欢这个答案,因为它是唯一能解决我在问题中提出的观点的答案。 如果有关于这些 DLL 技术的参考资料,以及不同操作系统之间的比较,将会很有趣。 看起来不错,但 CPU 速度肯定没有达到极限。【参考方案3】:

动态链接是满足某些许可要求(例如LGPL)的唯一实用方法。

【讨论】:

只要最终用户可以重新链接到 LGPL 的代码(例如,因为您在软件中提供了源代码或编译的目标文件),then static linking is fine。此外,如果您的软件供内部使用(即仅在您的组织内使用,而不是分发),那么您可以静态链接。这将适用于例如服务器软件,服务器不是分布式的。 不明白。您能否给我更多来源(或详细说明)以欣赏您所写的内容? @Thorn 请参阅LGPL license section 4.d+e。您要么需要以要求用户执行链接的形式分发,要么分发共享(动态)库。【参考方案4】:

我同意 dnmckee 提到的观点,另外:

静态链接的应用程序可能更容易部署,因为很少或没有额外的文件依赖项 (.dll / .so),当它们丢失或安装在错误的位置时可能会导致问题。

【讨论】:

值得注意的是,Google 的 Go 编译器出于这个原因静态编译二进制文件。【参考方案5】:

进行静态链接构建的一个原因是验证您是否已完全关闭可执行文件,即所有符号引用均已正确解析。

作为使用持续集成构建和测试的大型系统的一部分,夜间回归测试使用可执行文件的静态链接版本运行。有时,我们会看到符号无法解析,即使动态链接的可执行文件成功链接,静态链接也会失败。

这通常发生在共享库中根深蒂固的符号名称拼写错误且无法静态链接时。无论使用深度优先还是广度优先评估,动态链接器都不会完全解析所有符号,因此您可以完成一个没有完全闭包的动态链接可执行文件。

【讨论】:

【参考方案6】:

1/ 我一直在对动态链接和静态链接进行基准测试的项目中,差异没有确定到足以切换到动态链接(我不是测试的一部分,我只知道结论)

2/ 动态链接通常与 PIC(位置无关代码,不需要根据加载地址修改的代码)相关联。根据架构的不同,PIC 可能会带来另一次减速,但需要在两个可执行文件之间共享动态链接库(如果操作系统使用加载地址的随机化作为安全措施,甚至同一可执行文件的两个进程)。我不确定所有操作系统都允许将这两个概念分开,但 Solaris 和 Linux 可以,而 HP-UX 也可以这样做。

3/ 我参与过其他使用动态链接来实现“简单补丁”功能的项目。但是这个“简单的补丁”使得小补丁的分发变得更容易一些,而复杂的补丁则成为版本控制的噩梦。由于错误的版本是令牌,我们经常不得不推送所有内容以及必须在客户站点跟踪问题。

我的结论是我使用了静态链接例外:

诸如依赖动态链接的插件之类的东西

当共享很重要时(多个进程同时使用的大型库,例如 C/C++ 运行时、GUI 库……通常是独立管理的,并且 ABI 是严格定义的)

如果想使用“简易补丁”,我认为这些库必须像上面的大型库一样进行管理:它们必须几乎独立于已定义的 ABI,不得通过修复更改。

【讨论】:

一些用于非 PIC 或昂贵的 PIC 处理器的操作系统将准备动态库以加载到内存中的特定地址,如果他们可以这样做,他们只需将库的副本映射到与之相关的每一个过程。这大大减少了 PIC 的开销。至少 OS X 和一些 Linux 发行版会这样做,我不确定 Windows。 谢谢 Andrew,我不知道有些 Linux 发行版使用了这个。您是否有我可以关注的参考资料或我可以搜索以了解更多信息的关键词? (FWIW 我听说 Windows 正在做这个的变体,但 Windows 离我的能力范围太远了,我不提它)。 我认为您要查找的关键字是“prelink” - 它准备了一个库,以便在某个地址快速加载,以使程序启动更快。【参考方案7】:

动态链接的最佳示例是,当库依赖于使用的硬件时。在古代,C 数学库被认为是动态的,以便每个平台都可以使用所有处理器功能对其进行优化。

一个更好的例子可能是 OpenGL。 OpenGL 是一种由 AMD 和 NVidia 以不同方式实现的 API。而且您无法在 AMD 卡上使用 NVidia 实现,因为硬件不同。因此,您不能将 OpenGL 静态链接到您的程序中。这里使用动态链接让 API 针对所有平台进行优化。

【讨论】:

【参考方案8】:

其实很简单。当您对源代码进行更改时,您是要等待 10 分钟以构建它还是等待 20 秒?二十秒是我能忍受的。除此之外,我要么拔出剑来,要么开始考虑如何使用单独的编译和链接将其带回舒适区。

【讨论】:

我实际上并没有对编译速度的差异进行基准测试,但如果它明显更快,我会动态链接。 Boost 对我的编译时间做了足够多的坏事。【参考方案9】:

Static linking 是编译时的一个过程,当链接的内容被复制到主二进制文件中并成为单个二进制文件时。

缺点:

编译时间较长 输出二进制更大

Dynamic linking 是加载链接内容时的运行时进程。该技术允许:

升级链接二进制文件而不重新编译增加ABI稳定性[About]的主要二进制文件 只有一个共享副本

缺点:

开始时间较慢(应复制链接的内容) 运行时引发链接器错误

[ios Static vs Dynamic framework]

【讨论】:

【参考方案10】:

在类 Unix 系统上,动态链接会使“root”难以使用安装在偏僻位置的共享库的应用程序。这是因为动态链接器通常不会关注 LD_LIBRARY_PATH 或具有 root 权限的进程的等效项。有时,静态链接可以节省时间。

或者,安装过程必须找到库,但这会使多个版本的软件难以在机器上共存。

【讨论】:

关于LD_LIBRARY_PATH 的观点并不完全是使用共享库的障碍,至少在 GNU/Linux 中不是。例如。如果您将共享库放在相对于程序文件的目录 ../lib/ 中,那么使用 GNU 工具链,链接器选项 -rpath $ORIGIN/../lib 将指定从该相对位置搜索库。然后,您可以轻松地将应用程序与所有关联的共享库一起重新定位。使用这个技巧,拥有多个版本的应用程序和库也没有问题(假设它们是相关的,如果不是,你可以使用符号链接)。 > 用于具有 root 权限的进程。我认为您正在谈论从非 root 用户运行的 setuid 程序 - 否则这没有任何意义。在非标准位置包含库的 setuid 二进制文件很奇怪 - 但由于只有 root 可以安装这些程序,他也可以针对这种情况编辑 /etc/ld.so.conf【参考方案11】:

动态链接需要额外的时间让操作系统找到并加载动态库。使用静态链接,一切都在一起,它是一次性加载到内存中。

另外,请参阅DLL Hell。在这种情况下,操作系统加载的 DLL 不是您的应用程序附带的,也不是您的应用程序期望的版本。

【讨论】:

需要注意的是,有一系列的对策可以避免 DLL Hell。【参考方案12】:

另一个尚未讨论的问题是修复库中的错误。

使用静态链接,您不仅需要重建库,还必须重新链接和重新分发可执行文件。如果该库仅在一个可执行文件中使用,这可能不是问题。但是需要重新链接和重新分配的可执行文件越多,痛苦就越大。

使用动态链接,您只需重建和重新分发动态库即可。

【讨论】:

【参考方案13】:

静态链接将程序所需的文件包含在单个可执行文件中。

动态链接是您通常认为的,它生成的可执行文件仍然需要 DLL 等位于同一目录中(或者 DLL 可能位于系统文件夹中)。

(DLL = 动态链接库)

动态链接的可执行文件编译速度更快,而且资源较少。

【讨论】:

【参考方案14】:

静态链接只给你一个 exe,为了做出改变你需要重新编译你的整个程序。而在动态链接中,您只需要对 dll 进行更改,并且当您运行 exe 时,更改将在运行时获取。通过动态链接(例如:windows)更容易提供更新和错误修复。

【讨论】:

【参考方案15】:

在越来越多的系统中,极端水平的静态链接会对应用程序和系统性能产生巨大的积极影响。

我指的是通常被称为“嵌入式系统”的东西,其中许多现在越来越多地使用通用操作系统,而这些系统可用于任何可以想象的事情。

一个极其常见的例子是使用Busybox 的GNU/Linux 系统的设备。我通过构建包含内核及其根文件系统的可引导 i386(32 位)系统映像将 NetBSD 发挥到了极致,后者包含单个静态链接(由 crunchgen)二进制文件与所有程序的硬链接,这些程序本身包含所有(最后计数为 274)标准全功能系统程序(大多数除了工具链),并且小于 20 mega 字节大小(并且可能在只有 64MB 内存的系统中非常舒适地运行(即使根文件系统未压缩并且完全在 RAM 中),尽管我一直无法找到一个这么小的来测试它) .

在之前的帖子中已经提到,静态链接的二进制文件的启动时间更快(并且它可能会更快很多),但这只是图片的一部分,特别是当所有目标代码都链接到同一个文件时,尤其是当操作系统支持直接从可执行文件中按需分页代码时。在这种理想情况下,程序的启动时间字面意思可以忽略不计,因为几乎所有的代码页都已经在内存中并被 shell 使用(以及init 任何其他可能的后台进程) running),即使请求的程序自启动以来从未运行过,因为可能只需要加载一页内存即可满足程序的运行时要求。

但这还不是全部。我通常还通过静态链接所有二进制文件来为我的完整开发系统构建和使用 NetBSD 操作系统安装。尽管这需要更多的磁盘空间(x86_64 总共约 6.6GB,包括工具链和 X11 静态链接)(特别是如果一个完整的调试符号表可用于所有程序另外约 2.5GB),结果仍然整体运行速度更快,对于某些任务,甚至比声称共享库代码页的典型动态链接系统使用更少的内存。磁盘很便宜(即使是快速磁盘),缓存常用磁盘文件的内存也相对便宜,但 CPU 周期确实不便宜,并且为每个启动的进程支付ld.so 启动成本每个它启动的时间将花费数小时和数小时的 CPU 周期来完成需要启动许多进程的任务,尤其是当反复使用相同的程序时,例如开发系统上的编译器。静态链接工具链程序可以将我的系统的整个操作系统多架构构建时间缩短 小时。我还没有将工具链构建到我的单个 crunchgen'ed 二进制文件中,但我怀疑当我这样做时,由于 CPU 缓存的胜利,我会节省更多小时的构建时间。

【讨论】:

【参考方案16】:

另一个考虑因素是您在库中实际使用的目标文件(翻译单元)的数量与可用的总数。如果一个库是由许多目标文件构建的,但您只使用其中几个符号,这可能是支持静态链接的一个论据,因为您只链接您在静态链接时使用的对象(通常)并且不要t 通常带有未使用的符号。如果您使用共享库,则该库包含所有翻译单元,并且可能比您想要或需要的要大得多。

【讨论】:

以上是关于静态链接与动态链接的主要内容,如果未能解决你的问题,请参考以下文章

静态链接与动态链接

Linux动态链接和静态链接简析

静态链接与动态链接

如何使用cmake生成基于静态库的动态链接库

如何静态链接到 libstdc++.喜欢升级的GCC的朋友快来看看

[转载] 动态链接库dll的 静态加载 与 动态加载