用libabigail修剪动态重建
复杂的C ++项目经常与冗长的构建时间斗争。将项目分成多个动态链接组件可以为开发人员提供更快的增量重建和更短的编辑编译 - 测试周期,而不是依赖于静态链接,尤其是当有大量的测试二进制文件时。但是,由于如何处理传递库依赖性,构建系统通常不会实现动态增量重建中的所有可能的增益。Red Hat的Abi Introspection图书馆libabigail为某些源修改类提供了一种消除不必要的传递重链接的可能方法。
问题
考虑以下玩具项目包含两个库:libserver
和libclient
.服务器库libserver
取决于客户端库libclient
有关线路协议代码,以及客户端和服务器支持库实现,依赖于常用公用事件libcommon的库。这客户端
和服务器
可执行文件每个都使用关联的支持库。

通过考虑用于构建这些库的头文件和源文件,以及object文件等中间目标,我们可以看到更完整的依赖关系图。我们假设每个库有一个头文件和一个源文件。依赖关系图如下所示:

最后,我们假设一个构建系统,可以在以相同的结果重新生成依赖项时,可以使用内容签名来跳过重建。仅使用时间戳的构建系统无法大写下面概述的技术,因为重新生成的依赖关系总是具有更新的时间戳。
在这个环境中,如果我们做出有意义的改变,将重建什么libcommon.hpp
或libcommon.cpp.
,然后问客户端
和服务器
待建造的二进制文件?
好吧,改变libcommon.hpp
是一场灾难!
我们需要重新编译libcommon.cpp.
,生成一个新的libcommon.o.
,因此是一个新的libcommon。[a | SO]
.同样,自两而像libclient.cpp
和libserver.cpp.
取决于libcommon.h.
,他们需要重新编译并重建相关的库。自从此以来libserver
和libclient
支持库被重新连接,可执行文件现在超出日期,因此它们也会被重新连接。我们避免做的唯一工作正在重新编译client.cpp
和server.cpp.
,因为他们没有直接依赖libcommon.hpp
.哎哟。好吧,这是你的C ++。也许C ++模块将改善这种情况,但我们还没有生活在那个世界里。
下面的图表以图形方式展示了这一点,其中:
最黑暗的红色盒子是直接改变的实体。
中间的红色表示重新构建的实体,因为构建系统看到它的一个直接依赖项已更改。
最敏感的红色是由于隐式依赖性的变化,由构建系统被视为超出日期的未改变实体,这是一个隐式依赖性的。

变化的只是libcommon.cpp.
没有好多。我们避免需要重新编译自由{客户机、服务器}. cpp
,但我们仍然做了一堆遥感。以下是一种查找静态构建的方式:

请注意,我们在我们的图表中作弊:在静态构建中lib {client,server} .a
不要真的依赖libcommon.a.
.反而,服务器
和客户端
直接取决于它。所以这样的事实libcommon.a.
已更改不需要我们重新运行存档libclient.a
和libserver.a
.但这样画会让图更乱。
动态构建实际上在这里更糟糕,因为在这种情况下,我们确实需要重新链接libclient.so.
和libserver.so.
自他们的链接时间依赖libcommon.so
更新/更改。

可能,你可能会逃脱而不是重新克定客户端
和服务器
在动态情况下,由于重新链接libclient.so.
和libserver.so.
在这种情况下可能很好地产生相同的结果,并且基于内容签名的构建系统会注意到。但在实践中,libcommon.so
也很可能会出现在链接线上客户端
和服务器
,否则静态构建将无法工作。所以,除非你已经为静态和动态构建编写或生成了不同的库依赖项列表,libcommon.so
很可能是在链接线上客户端
和服务器
也是,使它们额外的链接时间伤亡。
洞察力
让我们想象一下libcommon.cpp.
是一些小而无害的东西,可能是在一个被记录的内部字符串常量中修复一个输入错误。在这样一个小示例中,我们需要重新链接这么多内容并不会太痛苦。但在一个更大的项目中,它肯定会造成伤害。为了这么小的改变而做这么多的链接,感觉是不对的。特别是在动态构建中,库依赖关系图中的一个小变化可能会导致一长串传递重链接,即使许多库完全没有改变。我们能做得更好吗?
对于静态链接,不,不是真的。更新后的字符串常量需要在两个可执行文件中都存在,所以我们真的需要重新链接它们,这样新的字符串常量就会被提取出来libcommon.a.
.
通过动态链接,事实证明我们可以做得更好。关键观察是考虑如果我们重建时会发生什么只要libcommon.so
,并且故意不重新链接其他动态库或可执行文件(即使构建系统认为我们应该这样做),然后试图运行可执行文件。它们会像预期的那样有效吗?
对于我们提出的私有字符串不断修改的情况libcommon.cpp.
,答案是一个明确的。更改内部字符串常量不会更改应用程序二进制接口(阿布斯) 的libcommon.so
以任何方式,当运行可执行文件时,字符串常量的更新值将反映在输出中,因为字符串常量未复制到可执行文件中:它在现在更换的情况下生活libcommon.so
.
我们逃脱了这一点,因为我们的变化没有改变abilibcommon.so
.如果我们改变了它的变化,重建了它的ABIlibcommon.so
然后,然后尝试运行可执行文件而无需重新链接它们,我们可能会看一下非常微妙的运行时崩溃。没有什么好玩的。
在理论上,您可以通过单独命名目标来最小化重新链接,以便在您知道您有ABI影响变化时构建。但在实践中显然出错,只是一个可怕的想法。但是如果我们有一个可以告诉我们一个图书馆的abi发生了变化的工具,那么我们可以教导我们的构建系统如何在其依赖步行中调用此工具,并在ABI保留情况下自动跳过任何不必要的重新链接修改。
一个办法
幸运的是,Red Hat已经提供了这样一个工具作为新图书馆的一部分libabigail
.正如他们描述的那样:
该项目旨在提供一个图书馆来操纵ABI Corpora,比较它们,提供有关其差异的详细信息,并帮助构建工具来推断有关这些差异的有趣结论。
这abidw.
附带的工具libabigail
读取共享库,查阅相关的库精灵和矮人一起编码与ABI相关的所有信息的信息,并发出描述库ABI的XML文档。
利用SCons的灵活性,我们可以增加它调用abidw.
并计算得到的ABI XML的哈希值,然后将该哈希值存储在库的旁边的一个文件中。当另一个目标声明它链接到库时,我们告诉scon记录对ABI哈希文件的依赖关系,而不是对库本身的依赖关系。因此,如果库被重新链接,但它的ABI没有改变,那么ABI哈希文件将具有相同的内容。因为SCons使用内容签名来检测目标是否过期,所以ABI哈希文件被视为最新的,即使它是重新生成的。由于该依赖项被视为最新的,因此依赖目标也被视为最新的。ABI保存对库的修改不再导致依赖项重新链接!
下图更新了我们的原件,包括关联的ABI哈希文件,并将其删除出现在现在无益的源和标题中。我们现在还区分了依赖关系(实线)和链接到/需要关系(虚线):

现在,如果我们做出了影响的改变libcommon.so
,我们可以看到libclient.so.
和libserver.so.
被重新连接。但libclient.so.
和libserver.so.
只使用libcommon.so
在内部,所以他们的abi没有改变。这客户端
和服务器
可执行文件不需要重新链接:

另一方面,如果我们改变了libcommon.so
不影响ABI,则没有其他内容被重新链接:

正确性
这样做是安全的吗?我们相信它是。我们还没有想到任何情况下的情况。另外,假阳性(例如,ABI在没有时改变的声明)只需花费我们错过的优化。假阴性会有害,但会代表一个严重的错误libabigail
.此外,我们目前只提供这个功能作为一个可选择的开发者构建;我们交付给客户的构建并不使用它。
然而,有一些重要的正确性问题需要注意:
- 与互动差
-gsplit -dwarf.
旗帜调试裂变.libabigail
用来elfutils
图书馆为其矮化处理,和elfutils
还是不知道如何去接触.dw {o p}
文件,-gsplit -dwarf.
和相关的工具创建。自libabigail
依靠矮人信息来识别ABI,运行abidw.
在一个由对象构建的库上-gsplit -dwarf.
提供不正确的结果。所以你不能同时使用ABI驱动的链接和调试分裂。据推测,随着对新的DWARF 5标准的支持的加入,这个限制将被取消elfutils
.
- 这
libabigail
图书馆是新的,我们在与我们的图书馆合作时,我们有几个实例在哪里崩溃。Dodji Seketeli,作者libabigail
,一直非常有帮助,响应于这些崩溃,但您需要有一个相当流血的边缘版本abidw.
如果您希望此技术在实践中运行良好,可用。
- 要充分利用这项技术,需要正确地将符号可见性注释应用于类型和函数定义,而且通常所有代码都可以用它构建
-fvisibility-hidden
.否则,实际上,实际上不会形成图库的部分的实体仍然导出,因此被视为abi的一部分libabigail
,导致杂散的重叠。
表现
这个解决方案是否表现?不幸的是,答案现在是一个响亮的“它取决于”。
这蒙戈:状态
类是编译的库,其中几乎所有其他库和MongoDB服务器项目中的所有库和可执行文件所依赖的库。188金宝搏手机客户端安卓下载在将非ABI更改的编辑到其实现文件进行后,使用时,我的机器上的所有目标的重建速度快40%abidw.
跳过链接比不。这是一个相当令人信服的胜利,但也是最好的情况。
最坏的情况是相当糟糕。运行的成本很大abidw.
在每个库。对于一些复杂的库,它可能非常慢:运行abidw.
在SpiderMonkey JS引擎上需要30秒以上。在总计算时间方面,完全重链接abidw.
大约需要两倍的CPU时间作为完整的重新链接而没有。另一种看着它的方式是跑步abidw.
关于第二次链接的昂贵。所以使用abidw.
如果您在链接图中深入了解,您的工作承认高度的链接避免,如果您正在进行会导致大量ABI的工作,可能是值得的。不幸的是,很难知道你可能会做的事情。另一方面,如果你有树的子集,那么不经常改变,那些子集的成本在许多构建中摊销。
总的来说,在性能方面可能需要进一步的工作。
或者,也许不是......
如果适当的头纪律是完全在一个代码库,每个ABI相关函数或对象都有一个独特的声明在一个标题,应该不可能使一个源代码改变导致ABI变异在图书馆但不导致所有依赖库或程序被重建。在这样的环境中,应该可以削弱用于链接共享库的构建系统规则,以诱导仅顺序关系,而不是严格依赖关系。这样就完全不需要使用了libabigail
检测ABI变异。期待这样的纪律是合理的吗?有机械强制执行方法吗?尽管如此,是否有办法颠覆ABI兼容性?C ++模块是否会提供该功能?根据这些和类似问题的答案,投资时间更好地实现该方法和相关的工具,而不是依赖于ABI元数据。
未来发展方向
如果使用ABI元数据确实被证明是正确的方法,那么当前的实现有一些地方需要改进:
- 根据上面的讨论,一般改善了速度
abidw.
是必要的。改善性能的其他潜在途径可能包括编写一个编译器或链接器插件,这些插件可以发出类似于由此产生的ABI描述abidw.
同时执行链接步骤,避免了第二次传递的需要abidw.
.
- 我们目前在SCONS工具的命令正文中使用管道和文件重定向的做法是有点危险.我们可以把它改成只把完整的XML发送到
.abidw.
文件通过abidw——out-file
选项,并允许SCON内部签名生成机制来计算哈希。但是,这将结束撰写数百兆字节的信息,我们实际上并不关心磁盘,并杂烩冒险缓存。可能,添加压缩选项abidw.
将是一种有效的补救措施。的总大小abidw.
MongoDB的完整构建生成的数据是203mb,但是非常简单188金宝搏手机客户端安卓下载GZIP.
将每个文件的大小压缩到14 MB。
- 改善性能的另一种选择是完全消除XML生成。我们目前的工作比需要更多,因为我们正在生成XML,然后才能将右转进入MD5以计算签名。如果是
libabigail
图书馆有一个ab
直接发出签名的程序,我们可能会在一定程度上提高工具的性能。
- 我们不仅可以检测ABI是否发生了变化,还可以使用其他方法来消除更多的重建
libabigail
公用事业喜欢亚偶
要执行ABI兼容性检测,并且只有在依赖性中存在非ABI兼容变更时才重新链接。这将允许将新函数添加到库,而无需依赖于重新链接的那些新函数的库。我们将来会调查这一点,但它可能需要一个明显更复杂的构建系统集成。
- 这
libabigail
库目前只能在ELF平台上运行。有可能让它在麦斯斯工作因为它的调试信息也是矮人,但它需要重大努力使它与之合作Mach-O.二进制的一部分。它肯定不支持Windows,尽管我很好奇作为DLL构造的一部分生成的导入库是否包含足够的信息来识别ABI。如果你知道,请联系我,让我知道,或者回复关于这个话题的StackOverflow问题.
结论
总的来说,这种避免再链接的方法是否成功?我们目前的观点是,该工具不足以作为开发人员构建的默认部署来获得一致的胜利,但潜在的收益是足够引人注目的,我们将继续追求上述的性能改进和未来方向。
应该libabigail
证明是正确的方法,我们打算在SCons集成上投入时间,以解决上面提到的一些缺陷和限制。如果这些问题能够得到满意的解决,我们最终希望看到该工具合并到scon主线中。我们也希望与之合作libabigail
维护人员进一步改进其特性集和性能。
最后,即使使用具体方法abidw.
为了跳过重新链接证明不可行,我们对偶然发展的见解感到满意,这些见解是关于标题纪律和联系的洞察力,这可能最终提供零成本的方式来实现相同的目标。