C ++链接是否足够智能以避免未使用的库链接?

时间:2014-09-08 10:07:56

标签: c++ linker

我还远未完全理解C ++链接器的工作原理,我对此有一个特定的问题。

说我有以下内容:

Utils.h

namespace Utils
{
    void func1();
    void func2();
}

Utils.cpp

#include "some_huge_lib" // needed only by func2()

namespace Utils
{
    void func1() { /* do something */ }
    void func2() { /* make use of some functions defined in some_huge_lib */ }
}

的main.cpp

int main()
{
  Utils::func1()
}

我的目标是生成尽可能小的二进制文件。

我的问题是,some_huge_lib是否会包含在输出对象文件中?

4 个答案:

答案 0 :(得分:37)

包含或链接大型图书馆通常不会有所作为,除非你使用那些东西。链接器执行死代码消除,从而确保在构建时不会获得包含大量未使用代码的大型二进制文件(请阅读编译器/链接器手册以了解更多信息,这不是& #39; t由C ++标准强制执行。)

包含大量标题也不会增加您的二进制文件大小(但它可能会大大增加您的编译时间,cfr。预编译标头)。一些例外代表全局对象和动态库(那些不能剥离)。我还建议to read this passage gcc only )将代码分成多个部分。

关于演出的最后一个通知:如果你使用很多的位置相关代码(即代码不能映射到具有相对偏移的任何地址,但需要一些' hotpatching& #39;通过搬迁或类似的表格)然后会有启动成本。

答案 1 :(得分:22)

这取决于 lot 您使用哪些工具和开关进行链接和编译。

首先,如果链接some_huge_lib作为共享库,则需要在链接共享库时解析所有代码和依赖项。所以,是的,它会被拉到某个地方。

如果您将some_huge_lib链接为存档,那么 - 它取决于。读者的理智是将func1和func2放在单独的源代码文件中是很好的做法,在这种情况下,通常链接器将能够忽略未使用的目标文件及其依赖项。

但是,如果您在同一个文件中同时拥有这两个函数,那么在某些编译器上,您需要告诉它们为每个函数生成单独的部分。有些编译器会自动执行此操作,有些编译器根本不执行此操作。如果你没有这个选项,拉入func1将获取func2的所有代码,并且需要解决所有依赖项。

答案 2 :(得分:7)

将每个函数视为图中的节点 每个节点都与一段二进制代码相关联 - 节点功能的编译二进制代码 如果一个节点(函数)依赖于(调用)另一个节点(函数),则在两个节点之间存在链接(有向边)。

静态库主要是此类节点的列表(+索引)。

程序起始节点main()函数 链接器将图表从main()链接遍历到可执行文件中的所有可从main()访问的节点。这就是为什么它被称为链接器(链接映射可执行文件中的函数调用地址)。

未使用的功能,没有来自main()的图表中节点的链接 因此,这种断开连接的节点不可访问,并且不包括在最终可执行文件中。

可执行文件(与静态库相对)主要是从main()可以访问的所有节点的列表(+其他索引和启动代码)。

答案 3 :(得分:5)

除了其他回复之外,必须说通常链接器是按部分而不是函数工作的。

编译器通常可以配置它是否将所有目标代码放入一个整体部分或将其拆分为多个较小的部分。例如,打开拆分的GCC选项是-ffunction-sections(代码)和-fdata-sections(数据); MSVC选项为/Gy(两者都有)。 -fnofunction-sections-fnodata-sections/Gy-分别将所有代码或数据放入一个部分。

你可能会玩#39;在两种模式下编译模块然后转储它们(objdump用于GCC,dumpbin用于MSVC)以查看生成的目标文件结构。

一旦编译器形成一个部分,对于链接器它就是一个单元。节定义符号并引用其他节中定义的符号。链接器将在各部分之间建立依赖关系图(从多个根开始),然后解散或完全保留它们中的每一个。因此,如果某个部分中有一个已使用和未使用的函数,则将保留未使用的函数。

两种模式都有好处和缺点。转换分割意味着较小的可执行文件,但较大的目标文件和较长的链接时间。

还必须注意的是,在C ++中,与C不同,在某些情况下放宽了一个定义规则,并且允许函数或数据对象的多个定义(例如,在内联函数的情况下)。这些规则的制定方式允许链接器选择任何定义。

从部分的角度来看,将内联函数与非内联函数放在一起意味着在典型的使用场景中,链接器通常会被强制保留每个内联函数的几乎每个定义;这意味着过多的代码膨胀。因此,无论编译器命令行选项如何,这些函数和数据通常都会放在各自的部分中。

更新:正如@janm在评论中正确提醒的那样,还必须指示链接器通过指定--gc-sections(GNU)或/opt:ref来删除未引用的部分( MS)。