C++链接是否足够聪明,可以避免链接未使用的库



我还远远没有完全理解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会包含在输出对象文件中吗?

包含或链接大型库通常不会有什么不同,除非使用这些东西。链接器应该执行死代码消除,从而确保在构建时不会得到包含大量未使用代码的大型二进制文件(请阅读编译器/链接器手册了解更多信息,这不是C++标准强制执行的)。

包含大量的头也不会增加二进制大小(但可能会大大增加编译时间,cfr.precompiled头)。有些例外代表全局对象和动态库(不能剥离)。我还建议阅读这篇关于将代码分为多个部分的文章(gcconly)。

关于性能的最后一个注意事项是:如果您使用位置相关代码(即,不能仅映射到任何具有相对偏移量的地址,但需要通过重新定位或类似表进行一些"热修补"的代码),则会产生启动成本。

这在很大程度上取决于链接和编译所使用的工具和开关。

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

如果将some_huge_lib链接为存档,则-取决于具体情况。将func1和func2放在单独的源代码文件中是读者保持理智的好做法,在这种情况下,链接器通常可以忽略未使用的对象文件及其依赖项。

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

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

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

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

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

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

除了其他回复之外,必须指出的是,通常链接器是按节而非函数工作的。

编译器通常可以对其进行配置,无论是将所有对象代码放在一个单独的部分中,还是将其拆分为多个较小的部分。例如,打开拆分的GCC选项为-ffunction-sections(用于代码)和-fdata-sections(用于数据);MSVC选项为/Gy(对于两者)。-fnofunction-sections-fnodata-sections/Gy-,以将所有代码或数据放入一个部分。

您可以"玩"在两种模式下编译模块,然后转储它们(objdump用于GCC,dumpbin用于MSVC),以查看生成的对象文件结构。

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

这两种模式都有优点和缺点。打开拆分意味着可执行文件更小,但对象文件更大,链接时间更长。

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

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

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

相关内容

最新更新