静态链接与动态链接

491
在某些情况下,是否有充分的性能原因选择静态链接或动态链接?我听到或读到了以下内容,但我对此并不了解足够的知识来证明其真实性。
1)通常情况下,静态链接和动态链接之间的运行时性能差异微不足道。
2)如果使用利用档案数据来优化程序热点路径的 profiling 编译器,(1)不成立,因为在静态链接中,编译器可以优化您的代码和库代码。而使用动态链接仅可以优化您的代码。如果大部分时间花费在运行库代码上,这可能会产生很大的影响。否则,(1)仍然适用。

77
使用静态链接时,编译器可以优化库代码,但前提是编译器也要编译库代码!如果您只连接到预编译的目标文件,编译器将无法对其进行优化。 - Roger Pate
3
如果那是真的,那么你是正确的,但现代编译器是否完全正确还存在疑问,如果有人能够验证这一点,那就太好了。 - Eloff
5
大多数C/C++编译器将代码编译为本机代码,无法进行进一步的代码优化。如果将代码编译为某种中间语言(如.Net IL),则当库加载时会调用JIT编译器将其编译为本机代码。随着JIT编译器的不断演化,最终编译结果可以越来越好。 - Tarydon
3
如果启用LTCG,VS2008可以完全做到这一点。(虽然库文件会变得非常大..)我曾经尝试过它,对于那些对“我的编译器能为我做什么”感兴趣的人来说,它简直是神奇。 - peterchen
16个回答

424
  • 动态链接可以减少总资源消耗(如果多个进程共享同一个库(当然包括"相同版本")。我相信这是驱使其在大多数环境中存在的论点。这里的"资源"包括磁盘空间、RAM和缓存空间。当然,如果你的动态链接器不够灵活,那么就有DLL hell的风险。
  • 动态链接意味着库的漏洞修复和升级会传播您的产品,而无需您发货。
  • 插件始终要求使用动态链接。
  • 静态链接可以保证代码在非常有限的环境中运行(在启动过程早期或者救援模式中)。
  • 静态链接可以使二进制文件更容易分发到各种用户环境中(代价是发送一个更大、更需要资源的程序)。
  • 静态链接可能会使程序启动时间略微更快,但这在很大程度上取决于程序的大小和复杂性以及操作系统的加载策略的细节。

一些编辑,包括评论和其他答案中非常相关的建议。需要注意的是,你在此方面的断点方式很大程度上取决于你计划运行的环境。最小的嵌入式系统可能没有足够的资源支持动态链接。稍大一些的小型系统可能会支持动态链接,因为它们的内存足够小,使动态链接的RAM节省非常有吸引力。完整的消费级PC具有巨大的资源,正如马克指出,你可能可以让便利问题来推动你对这个问题的思考。


为了解决性能和效率问题:这要视情况而定。
传统上,动态库需要某种粘合层,这通常意味着双重分派或函数寻址中的额外间接层,并可能会稍微降低速度(但是函数调用时间实际上是否占用了您的运行时间的大部分?)。
然而,如果您运行多个进程都频繁调用同一库,则使用动态链接相对于使用静态链接可以节省缓存行(从而在运行性能上获胜)。 (除非现代操作系统足够聪明,能够注意到静态链接二进制文件中相同的段。这似乎很困难,有人知道吗?)
另一个问题:加载时间。您在某个时候需要支付加载成本。何时支付此成本取决于操作系统的工作方式以及您使用的链接方式。也许您希望推迟支付直至需要它。
请注意,静态与动态链接传统上并不是优化问题,因为它们都涉及单独编译到对象文件。但是,这不是必需的:原则上,编译器可以将“静态库”“编译”为消化的AST形式,并通过将这些AST添加到生成的主代码AST中来“链接”它们,从而增强全局优化。我使用的系统都没有这样做,所以我无法评论它的工作效果。
回答性能问题的方法始终是通过测试(并尽可能使用与部署环境相似的测试环境)。

32
资源消耗基本上是指代码空间,随着时间的推移,这不再是一个越来越值得关注的问题。如果500K的库被5个进程共享,那么可以节省2MB的空间,这相当于3GB内存的不到0.1%。 - Mark Ransom
4
如果库也共享同一虚拟映射(即在所有进程中具有相同的物理和虚拟地址),那么动态链接是否也能在处理器的MMU中节省TLB槽位? - Zan Lynx
10
同时,动态链接使得更新有缺陷的库代码变得更加容易,使用更好的版本来替换原有的代码。 - Zan Lynx
104
它还使得在一个工作版本中添加有缺陷的代码变得容易。 - anon
9
“插件总是要求动态链接。”这是不正确的说法。一些插件模型,例如苹果的音频单元(AudioUnits),可以在单独的进程中运行插件并使用IPC。这是插件的一个更安全的替代方案(插件无法使主机崩溃)。建议将答案更新为“插件可能需要动态链接”或类似的说法。 - Taylor
显示剩余20条评论

78

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

2) 这是正确的。通过优化来自性能分析的指导,您通常可以获得10-15%的性能提升。现在CPU速度已经达到极限,这可能值得一试。

我想补充一点:(3)链接器可以将函数排列在更高效的缓存分组中,以使昂贵的缓存级别失误最小化。它也可能特别影响应用程序的启动时间(基于我用Sun C ++编译器看到的结果)

而且不要忘记,对于DLLs,无法执行死代码消除。根据语言,DLL代码可能也不是最优的。虚拟函数始终是虚拟的,因为编译器不知道客户端是否正在覆盖它。

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

编辑(回答用户underscore的评论)

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

如已解释,x86除了15位跳跃范围外,似乎没有其他用途,并且不适用于无条件跳转和调用。这就是为什么拥有超过32K的函数(从生成器中)一直是个问题并需要嵌入式跳板。

但是在像Linux这样的流行x86操作系统上,您不需要担心.so/DLL文件是否使用gcc开关-fpic生成(该开关强制使用间接跳转表)。因为如果您不这样做,代码就像普通链接器一样被修复。但是在执行此操作时,它会使代码段无法共享,并且需要将代码从磁盘完全映射到内存中并触摸所有内容,然后才能使用它(清空大部分缓存,命中TLB等)。有一段时间,这被认为很慢。

因此,您将不再获得任何好处。

我不记得我的Unix构建系统在哪个操作系统(Solaris或FreeBSD)上给我带来了问题,因为我没有这样做,直到我向gcc应用了-fPIC,才想到为什么会崩溃。


6
我喜欢这个答案,因为它是唯一一个回答了我在问题中提出的要点。 - Eloff
有关那些DLL技术细节的参考资料以及不同操作系统之间的比较将是非常有趣的。 - UncleZeiv
1
看起来还不错,但 CPU 速度肯定没有达到极限。 - Aidiakapi

72

动态链接是满足某些许可证要求的唯一实用方式,例如LGPL


19
只要最终用户可以重新链接到LGPL的代码(例如,因为您提供了源代码或编译后的目标文件与您的软件一起),那么静态链接是可以的。此外,如果您的软件仅供内部使用(即仅在您的组织内部使用,而不分发),则可以进行静态链接。这适用于服务器软件等情况,其中服务器未经分发。 - JBentley
3
不理解,请您提供更多来源(或详细说明),以便于我理解您所写的内容。 - Baskaya
4
@Thorn,请查看LGPL许可证第4.d+e节。您需要采用一种要求用户链接的形式进行分发,或者分发一个共享(动态)库。 - Mark Ransom

47

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

  • 静态链接的应用程序可能更容易部署,因为它们没有或减少了附加文件依赖(.dll/.so)。如果这些文件缺失或安装在错误的位置可能会引起问题。

8
值得注意的是,来自Google的Go编译器仅出于这个原因会静态编译二进制文件。 - Hut8

35

进行静态链接的一个原因是为了验证可执行文件的完全封闭性,即所有符号引用都被正确解析。

在使用持续集成构建和测试的大型系统中,每晚的回归测试都是使用静态链接版本的可执行文件运行。偶尔会发现一个符号无法解析,导致静态链接失败,尽管动态链接的可执行文件能够成功链接。

通常情况下,这种情况发生的原因是共享库中深层嵌套的符号名称拼写错误,因此无法静态链接。动态链接器无论使用深度优先还是广度优先评估,都无法完全解析所有符号,因此您可能会得到一个没有完全封闭性的动态链接的可执行文件。


21

1/ 我曾参与过一些项目,对动态链接和静态链接进行了基准测试,结果发现两者之间的差异不足以使我们转向使用动态链接(我没有参与测试,只是知道结论)。

2/ 动态链接通常与PIC(位置无关代码)相关联,它是一种不需要根据加载地址进行修改的代码。根据体系结构的不同,PIC可能会带来额外的性能下降,但是为了共享动态链接库的好处是必需的,这适用于两个可执行文件之间甚至是同一可执行文件的两个进程(如果操作系统将加载地址随机化作为安全措施)。我不确定所有操作系统是否允许分离这两个概念,但Solaris和Linux可以,我记得HP-UX也可以。

3/ 我曾参与过其他项目,其中使用动态链接来实现“轻松补丁”功能。但是这种“轻松补丁”会使小修复的分发变得更加容易,而复杂修复的版本控制则会变成噩梦。我们经常不得不推出所有东西,而且还要在客户现场跟踪问题,因为使用了错误的版本。

我的结论是,我会使用静态链接,除非:

  • 像依赖于动态链接的插件之类的东西

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

如果要使用“轻松补丁”,我认为库必须像上述大型库那样进行管理:它们必须几乎是独立的,并具有不应通过修复而更改的定义良好的ABI。


1
一些非PIC或昂贵的PIC处理器的操作系统将准备动态库以在内存中的特定地址加载,如果可以这样做,它们只会将库的副本映射到与之链接的每个进程中。这大大降低了PIC的开销。至少OS X和一些Linux发行版是这样做的,我不确定Windows是否也是如此。 - Andrew McGregor
谢谢Andrew,我不知道有些Linux发行版使用了这个。你有参考资料或者关键词可以让我学习更多吗?(顺便说一句,我听说Windows也在做这方面的变体,但是Windows距离我的专业领域太远了,所以我没有提到它)。 - AProgrammer
我认为你要找的关键字是“prelink” - 它可以准备一个库以在特定地址快速加载,从而加快程序启动速度。 - Blaisorblade

14

静态链接是在编译时将链接内容复制到主二进制文件中,形成一个单一的二进制文件。

缺点:

  • 编译时间较长
  • 输出二进制文件较大

动态链接是在运行时加载链接的内容。该技术允许:

  • 升级链接的二进制文件而无需重新编译主二进制文件,从而增加ABI稳定性[相关]
  • 有单个共享副本

缺点:

  • 启动时间较慢(链接内容应被复制)
  • 链接器错误在运行时抛出

[iOS静态框架 vs 动态框架]


12

其实很简单。当您在源代码中进行更改时,您想等待10分钟才能构建完成还是20秒钟?我只能容忍20秒钟的时间。超过这个时间,我要么找出解决方案,要么考虑如何使用分离编译和链接来使它回到舒适区。


1
我实际上还没有对编译速度的差异进行基准测试,但如果动态链接能够显著提高速度,我会选择它。Boost已经让我的编译时间变得足够糟糕了。 - Eloff

12

动态链接的最佳例子是,当库依赖于使用的硬件时。在古代,决定C数学库是动态的,这样每个平台都可以利用所有处理器功能来优化它。

甚至更好的例子可能是OpenGL。OpenGL是一个由AMD和NVidia不同实现的API。您无法在AMD卡上使用NVidia实现,因为硬件不同。你不能将OpenGL静态链接到你的程序中,因为这个原因。动态链接在这里被用来让API为所有平台进行优化。


9
动态链接需要额外的时间让操作系统找到动态库并加载它。静态链接则将所有内容放在一起,只需一次性加载到内存中。
此外,请参见DLL Hell。这是操作系统加载的DLL不是您的应用程序所带的DLL,或者不是您的应用程序所期望的版本的情况。

1
需要注意的是,有许多应对措施可避免 DLL Hell 问题。 - ocodo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接