由于调试符号,巨大的可执行文件,为啥?

Posted

技术标签:

【中文标题】由于调试符号,巨大的可执行文件,为啥?【英文标题】:huge executables because of debugging symbols, why?由于调试符号,巨大的可执行文件,为什么? 【发布时间】:2011-09-03 22:19:44 【问题描述】:

我们一直在一家银行开发一个大型金融应用程序。它开始是 150k 行非常糟糕的代码。到 1 个月前,它下降到一半多一点,但可执行文件的大小仍然很大。我希望我们只是让代码更具可读性,但模板化代码仍在生成大量目标代码,我们只是在努力提高效率。

应用程序分为大约 5 个共享对象和一个主对象。更大的共享对象之一是 40Mb,即使代码缩减,也增长到 50。

代码开始增长我并不完全感到惊讶,因为毕竟我们正在添加一些功能。但令我惊讶的是它增长了 20%。当然,没有人接近编写 20% 的代码,所以我很难想象它是如何增长这么多的。那个模块对我来说有点难以分析,但在星期五,我有一个新的数据点,可以提供一些启示。

SOAP 服务器可能有 10 个提要。代码是自动生成的,很糟糕。每个服务都有一个解析器类,其代码完全相同,例如:

#include <boost/shared_ptr.hpp>
#include <xercesstuff...>
class ParserService1 
public:
  void parse() 
    try 
      Service1ContentHandler*p = new Service1ContentHandler( ... );
      parser->setContentHandler(p);
      parser->parser();
     catch (SAX ...) 
      ...
    
  
;

这些类是完全没有必要的,一个函数就可以工作。每个 ContentHandler 类都是使用相同的 7 或 8 个变量自动生成的,我可以通过继承共享这些变量。

因此,当我从代码中删除解析器类和所有内容时,我期望代码的大小会下降。但是只有 10 个服务,我没想到它会从 38Mb 下降到 36Mb。符号数量多得离谱。

我唯一能想到的是每个解析器都包含 boost::shared_ptr、一些 Xerces 解析器的东西,而且不知何故,编译器和链接器为每个文件重复存储所有这些符号。无论如何,我很想知道。

那么,谁能建议我如何去追查为什么像这样的简单修改会产生如此大的影响?我可以在模块上使用 nm 来查看里面的符号,但这会产生令人痛苦的、大量半可读的东西。

此外,当一位同事使用我的新库运行她的代码时,用户时间从 1 分 55 秒变为 1 分 25 秒。实时是高度可变的,因为我们正在等待缓慢的 SOAP 服务器(恕我直言,SOAP 是 CORBA 的一个非常糟糕的替代品......),但 CPU 时间相当稳定。我本来希望减少这么多代码大小会带来轻微的提升,但最重要的是,在具有大量内存的服务器上,考虑到我没有改变架构,我真的很惊讶速度受到如此大的影响XML 处理本身。

我将在周二更进一步,希望能获得更多信息,但如果有人知道我如何才能取得如此大的进步,我很想知道。

更新: 我验证了事实上,在任务中使用调试符号似乎根本不会改变运行时间。我通过创建一个包含很多东西的头文件来做到这一点,包括在这里产生影响的两个:boost shared pointers 和一些 xerces XML 解析器。似乎没有运行时性能受到影响(我检查过,因为两个答案之间存在意见分歧)。但是,我还验证了包含头文件会为每个实例创建调试符号,即使剥离的二进制大小没有改变。因此,如果您包含给定文件,即使您甚至不使用它,也会有固定数量的符号反对该对象,即使它们可能相同,它们在链接时也不会折叠在一起。

我的代码如下:

#include "includetorture.h"
void f1()

    f2(); // call the function in the next file

我的特定包含文件的大小约为每个源文件 100k。据推测,如果我包含更多,它会更高。包含的总可执行文件约为 600k,没有大约 9k。我验证了增长与包含文件的数量成线性关系,但剥离后的代码大小是相同的,应该是这样。

显然,我错误地认为这是性能提升的原因。我想我现在已经考虑到了。尽管我没有删除太多代码,但我确实简化了很多大的 xml 字符串处理,并且大大减少了通过代码的路径,这大概就是原因。

【问题讨论】:

在你的标题中你提到了调试符号,但你没有在你的帖子的其余部分。我错过了什么吗? @Bart 膨胀是因为可执行文件中的所有调试符号。如果剥离库,代码大约是大小的 10%。 【参考方案1】:

您似乎使用了很多带有内联方法的 c++ 类。如果这些类具有很高的可见性,则此内联代码将使整个应用程序膨胀。我敢打赌你的链接时间也增加了。尝试减少内联方法的数量并将代码移至 .cpp 文件。这将减少您的目标文件、exe 文件的大小并减少链接时间。

这种情况下的权衡当然是减少编译单元的大小,而不是执行时间。

【讨论】:

如果是 xerces 和 boost,我不确定是否要编辑他们的东西,但我会看看。 如果代码是内联的,并且没有被调用,应该没有效果。我怀疑这是由于全局变量,并引入了它们引用的所有符号。但我不知道为什么链接器会在可执行文件中保留多个副本——删除它们可能太贵了。 当然,但是内联代码会在许多执行单元(跨多个目标文件)中生成代码,如果不进行优化,将导致生成的可执行文件增加。请记住,内联代码是内联的,权衡(总是有权衡)是更快的执行时间与函数调用开销。 内联函数如果不调用就不会产生任何东西,所以你刚才说的不应该是真的,不需要任何优化。事实上,这似乎是真的。不必要地加载了有关数据类型的所有符号,并且没有人在链接时为删除冗余副本而烦恼。 @Dov:我假设调用了内联函数。你完全正确。【参考方案2】:

您可以使用 linux 上的 readelf 实用程序或 windows 上的 dumpbin 来查找 exe 文件中各种数据使用的确切空间量。不过,我不明白为什么可执行文件的大小让您担心:调试符号在运行时绝对不使用内存!

【讨论】:

显然,仅仅生成这么多的东西需要时间来构建。我担心趋势。这是,减少了代码大小,但可执行文件膨胀了 20%。不过很高兴知道它们不是在运行时加载的...... C++ 符号变得异常长,因为名称被修改了;这在使用 boost 时尤其明显。一个看似“简单”的类型,例如一些 boost 模板的实例化,可能会产生几 kB 长的错位名称!在这种情况下,调试信息的大小取决于不同实体(方法、函数、实例化模板)的数量。这是一个已知问题,一些 boost 库在其手册中解决了减少符号名称长度的问题。但是,同样,您为什么要尝试减少代码大小? 我很感兴趣,因为我注意到精简几百行不应该对整个系统产生太大影响的代码对总符号大小和性能产生了巨大影响的应用程序。如果我能发现导致膨胀的原因并知道如何避免它,那么任务将运行得更快一些。这不是一个微不足道的问题,因为我们必须在生产环境中运行调试(以便在出现问题时可以调试代码),如果我能获得速度上的胜利,总体而言,这对测试和我们的夜间测试都是有利的批处理 1) 符号大小不会对性能产生任何影响。或者,更确切地说,如果确实如此,那么您有一个非常、非常、非常严重损坏/配置错误的操作系统。 2) 可执行文件大小与性能几乎没有任何显着相关性:大数字的分解就是一个很好的例子。 3) 你的绩效指标是什么,你如何衡量它? 4) 做一个实验:从你的可执行文件中去掉所有的调试符号(这个实用程序被方便地命名为strip),看看它是否对性能有任何影响。 我刚用g++ 4.5.1编译,代码缩水了30%。所以新版本在调试器中去掉了很多多余的符号。正如我们在讨论中所预期的那样,性能没有变化【参考方案3】:

对于您的问题,我没有您所期望的答案,但让我分享一下我的经验。

可执行文件的大小差异很大是很常见的。我无法详细解释原因,但想想现代调试器让你在代码上做的所有疯狂的事情。你知道,这要归功于调试符号。

大小差异如此之大,如果您要动态加载某些共享库,那么文件的绝对加载时间可以解释您发现的性能差异。

确实,这是编译器的一个非常“内部”方面,举个例子,几年前我对 GCC-4 生成的巨大可执行文件与 GCC-3 相比非常不满意,然后我只是习惯了(我的 HD 也变大了)。

总而言之,我不介意,因为您应该只在开发期间使用带有调试符号的构建,这不应该成为问题。在部署中,没有调试符号,你会看到文件会缩小多少。

【讨论】:

sergio,经过测试,这似乎不是真的。可执行文件大小与速度无关,至少在我做的小测试中是这样。这不是随机的,我可以做一个实验,如果不愉快,它是可重复的。 @Dov,感谢您检查这一点。其实我的意思是加载时间。如果您有多个共享库,每个大小为 10MB 且充满符号,并且您动态加载它们,那么在加载它们的那一刻,您的程序将因为动态链接而变慢。如果你算上十次减速,你会得到一些明显的东西。但这只是一个想法,因为我不知道您的应用程序是如何构建的。无论如何,由于您剥离了可执行文件,我知道加载时间也不会影响您的应用程序。 我的测试当然是在小得多的文件上进行的,但我们谈论的是一项运行时间为 10 分钟的大型工作,因此即使加载了几秒钟,100Mb 也不应该产生了很大的影响,事实上并没有。

以上是关于由于调试符号,巨大的可执行文件,为啥?的主要内容,如果未能解决你的问题,请参考以下文章

关于QtCreator中三种不同编译版本 debugreleaseprofile 的区别

golang 环境build之后可执行文件为啥没有在bin生成

避免从 Linux 上的可执行文件中导出符号

调试没有符号的核心文件

未找到 GraphViz 的可执行文件 - 为啥通过 pip 安装 graphViz 后没有安装可执行文件?

为啥 AssocQueryString 找不到与图像扩展关联的可执行文件?