DHAT:动态堆分析工具

本文介绍DHAT工具,用于检查程序的堆分配使用情况。通过跟踪分配的块和内存访问,帮助识别潜在的生命周期泄漏、过度瞬态分配等问题。文章详细解释了max-live、tot-alloc等字段含义及比例字段的意义。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

要使用此工具,必须--tool=exp-dhat在Valgrind命令行上指定 。

10.1。概观

DHAT是一个用于检查程序如何使用其堆分配的工具。

它跟踪分配的块,并检查每个内存访问以查找哪个块(如果有的话)。每个分配点(分配堆栈)收集和显示以下数据:

  • 总分配(字节数和块数)

  • 最大实际体积(字节数和块数)

  • 平均块寿命(分配和释放之间的指令数)

  • 块中每个字节的平均读写次数(“访问率”)

  • 对于始终分配只有一个大小的块的分配点,大小为4096字节或更少:计数表示访问块内每个字节偏移的频率。

使用这些统计信息可以识别具有以下特征的分配点:

  • 潜在的过程生命周期泄漏:由点分配的块刚刚累积,仅在运行结束时释放。

  • 过多的营业额:即使没有持续很长时间,咀嚼了很多堆的点数

  • 过度瞬态:分配非常短暂的块的点

  • 无用或未充分使用的分配:分配但未完全填充或填写但未随后读取的块。

  • 具有低效布局的区域 - 从未访问的区域,或者散布在整个块中的热场。

与Massif堆分析器一样,DHAT通过计数指令来测量程序进度,因此将所有年龄/时间相关数字作为指令计数。这首先听起来有点奇怪,但是如果使用CPU时间,则使得运行可重复的方式是不可能的。

10.2。了解DHAT的输出

DHAT提供了大量有关动态堆使用的有用信息。大多数使用它的艺术是解释结果的数字。这是通过一组示例来说明的。

10.2.1。解释max-live,tot-alloc和deaths字段

10.2.1.1。一个简单的例子
   ========摘要统计========

   guest_insns:1,045,339,534
   [...]
   最大活动:63,490,984个块
   tot-alloc:29,520块中的1,904,700(平均尺寸64.52)
   死亡人数:29,520人,平均22,227,424人
   比例:6.37 rd,1.14 wr(12,141,526 b-read,2,174,460 b-written)
      在0x4C275B8:malloc(vg_replace_malloc.c:236)
      通过0x40350E:tcc_malloc(tinycc.c:6712)
      通过0x404580:tok_alloc_new(tinycc.c:7151)
      by 0x40870A:next_nomacro1(tinycc.c:9305)

在整个程序中,这个堆栈(分配点)共分配了29,520个块,总共包含1,904,700个字节。通过查看max-live数据,我们看到没有多少块同时生效,但是在高峰期,984个块中有63,490个分配的字节。这告诉我们,该程序正在稳定地释放这样的块,而不是挂在所有的块上,直到结束并释放它们。

死亡条目告诉我们,这个堆栈分配的29,520个块在程序运行期间死了(被释放)。由于29,520也是总共分配的块的数量,这告诉我们所有分配的块在程序结束时被释放。

它也告诉我们,平均死亡年龄为22,227,424个指示。从总结统计,我们看到,该计划运行了1,045,339,534个指令,因此平均死亡年龄约为程序总运行时间的2%。

10.2.1.2。潜在的过程寿命泄漏示例

下一个示例(来自与上述不同的程序)显示潜在的进程生命周期泄漏。当程序继续分配数据时,会发生进程生命周期泄漏,但只能在退出之前释放数据。因此,程序堆的大小不断增加,但是Memcheck报告没有泄漏,因为程序在退出时已经释放了一切。这对于长时间运行的程序尤其危险。

   ========摘要统计========
   
   guest_insns:418,901,537
   [...]
   max-live:25412块中的32,512
   tot-alloc:25412块中的32,512(平均尺寸128.00)
   死亡人数:254人,平均年龄300,467,389人
   比例:0.26 rd,0.20 wr(8,756 b-read,6,604 b-written)
      在0x4C275B8:malloc(vg_replace_malloc.c:236)
      通过0x4C27632:realloc(vg_replace_malloc.c:525)
      by 0x56FF41D:QtFontStyle :: pixelSize(unsigned short,bool)(qfontdatabase.cpp:269)
      by 0x5700D69:loadFontConfig()(qfontdatabase_x11.cpp:1146)

有两个迹象表明这可能是一个生命周期的泄漏。首先,max-live和tot-alloc数字是相同的。可能发生的唯一方法是如果这些块都被分配,然后全部被释放。

其次,平均死亡人数(3亿人)是总计划生命周期的71%(4.1亿美元),因此这不是一个暂时的无分配飙升 - 而是分散在大部分整个运行。一个解释是,大概地,所有254个块都在上半场被分配到下一半,然后在退出之前释放。

10.2.2。解释比例字段

10.2.2.1。相当无害的分配点记录
   最大活动:59,398个808个块
   tot-alloc:24,240块中的1,481,940(平均61.13)
   死亡人数:24,240人,平均年龄34,611,026人
   比例:2.13 rd,0.91 wr(3,166,650 b-read,1,358,820 b-written)
      在0x4C275B8:malloc(vg_replace_malloc.c:236)
      通过0x40350E:tcc_malloc(tinycc.c:6712)
      通过0x404580:tok_alloc_new(tinycc.c:7151)
      通过0x4046C4:tok_alloc(tinycc.c:7190)

比例字段告诉我们,在块被释放之前,这里分配的块中的每个字节读取平均值为2.13倍。鉴于这些区块的平均死亡年龄为34,611,026,每块大约每1500个指令读取一次。所以从这个角度来看,这些区别并不“工作”。

更有趣的是写入比率:每个字节平均写入0.91次。这告诉我们,分配的块的一些部分永远不会被写入,平均至少有9%。要完全初始化块将需要写入每个字节至少一次,这将使写入比为1.0。一些块区明显未被使用的事实可能指向数据对齐孔或其他布局低效。

那么,至少所有的区块都被释放(24,240个分配,24,240个死亡)。

如果所有的块都是相同的大小,那么DHAT也会通过块偏移来显示访问计数,所以我们可以看到这些未使用的区域在哪里。但是,情况并非如此:块的大小不一样,所以DHAT无法进行这样的分析。我们可以看到它们必须具有不同的大小,因为平均块大小(61.13)不是整数。

10.2.2.2。一个更可疑的例子
   max-live:180,224在22个街区
   tot-alloc:22,2块中的180,224(平均尺寸8192.00)
   死亡:无(这些区块都没有被释放)
   比例:0.00 rd,0.00 wr(0 b-read,0 b-written)
      在0x4C275B8:malloc(vg_replace_malloc.c:236)
      通过0x40350E:tcc_malloc(tinycc.c:6712)
      0x40369C:__sym_malloc(tinycc.c:6787)
      通过0x403711:sym_malloc(tinycc.c:6805)

这里,读取和写入访问率都为零。因此,这一点是分配从未使用的块,既不读也不写。事实上,他们也没有被释放(“死亡:无”),只是泄漏了。所以,这里是180k的完全无用的分配,可以删除。

与Memcheck重新运行确实报告了同样的泄漏。什么DHAT可以告诉我们,Memcheck不能,不仅是块泄漏,它们也从未被使用过。

10.2.2.3。另一个可疑的例子

这里是一个分配块,写入但从不读取的地方。我们从零读取访问比率立即看到这一点。他们确实得到释放:

   max-live:54个3个块
   tot-alloc:1,020块(平均尺寸18.00)
   死亡人数:90人,平均年龄34,558,236人
   比例:0.00 rd,1.11 wr(0 b-read,1,800 b-written)
      在0x4C275B8:malloc(vg_replace_malloc.c:236)
      通过0x40350E:tcc_malloc(tinycc.c:6712)
      通过0x4035BD:tcc_strdup(tinycc.c:6750)
      通过0x41FEBB:tcc_add_sysinclude_path(tinycc.c:20931)

在前面的两个示例中,很容易看到从不写入或从不读取的块或两者的某种组合。不幸的是,在C ++代码中,情况不太清楚。这是因为一个对象的构造函数将写入底层的块,并且它的析构函数将从它读取。因此,即使该对象一旦构造,该块的读取和写入比率将不为零,也不会被使用,但只能最终被破坏。

真的,我们想要的只是在对象的构造结束和其破坏开始之间测量内存访问。不幸的是,我不知道确定何时进行这些转换的可靠方法。

10.2.3。解释“通过偏移量进行聚合访问计数”数据

对于总是分配相同大小的块,分配4096字节或更小的块的分配点,DHAT计数每个偏移量的访问,例如:

   max-live:317,408,5,668个块
   tot-alloc:317,408,5,668块(平均尺寸56.00)
   死亡人数:5,668人,平均622,890,597人
   比例:1.03 rd,1.28 wr(327,642 b-read,408,172 b-written)
      在0x4C275B8:malloc(vg_replace_malloc.c:236)
      通过0x5440C16:QDesignerPropertySheetPrivate :: ensureInfo(qhash.h:515)
      通过0x544350B:QDesignerPropertySheet :: setVisible(qdesigner_propertysh ...)
      通过0x5446232:QDesignerPropertySheet :: QDesignerPropertySheet(qdesigne ...)
   
   按偏移量进行聚合访问计数:
   
   [0] 28782 28782 28782 28782 28782 28782 28782 28782
   [8] 20638 20638 20638 20638 0 0 0 0 
   [16] 22738 22738 22738 22738 22738 22738 22738 22738
   [24] 6013 6013 6013 6013 6013 6013 6013 6013 
   [32] 18883 18883 18883 37422 0 0 0 0
   [36] 5668 11915 5668 5668 11336 11336 11336 11336 
   [48] 6166 6166 6166 6166 0 0 0 0 

对于在64位平台上运行的C ++代码,这是相当典型的。在这里,我们对5668个块的访问统计信息进行了汇总,大小为56字节。每个字节至少被访问5668次,除了偏移量12--15,36-39和52-55之外。这些可能是对准孔。

数字的仔细诠释揭示了有用的信息。以N对齐偏移开始的N个连续相同数字的组,对于N为2,4或8,可能在该点处的结构中指示N字节对象。例如,该对象的前32个字节可能具有布局

   [0] 64位类型
   [8] 32位类型
   [12] 32位定位孔
   [16] 64位类型
   [24] 64位类型

作为反例,还清楚的是,无论在偏移量32处,它不是32位值。那是因为最后一个组(37422)与前三个(18883 18883 18883)不一样。

这个例子引导一个人查询(通过读取源代码)12-15和52-55的零是否是对齐孔,以及48--51是否确实是一个32位类型。如果是这样,可能会将位置在48-51之间的位置替换为12--15,这样可以将对象大小从56个字节缩小到48个字节。

请记住,以上推论只是“可能”。这是因为它们基于动态数据,而不是对对象布局的静态分析。例如,零可能不是对齐孔,而只是结构的一部分,这些部分根本不用于此特定的运行。经验表明,不太可能是这种情况,但可能会发生。

10.3。DHAT命令行选项

DHAT特定的命令行选项有:

--show-top-n=<number> [default: 10]

在运行结束时,DHAT根据某个度量对累积的分配点进行排序,并显示最高的得分条目。 --show-top-n 控制显示的条目数。默认值为10是相当小的。对于现实的应用,您可能需要将其设置得更高,至少有几百个。

--sort-by=<string> [default: max-bytes-live]

在运行结束时,DHAT根据某个度量对累积的分配点进行排序,并显示最高的得分条目。 --sort-by 选择用于排序的度量:

max-bytes-live 最大活动字节[默认]

tot-bytes-allocd 字节总共分配(营业额)

max-blocks-live 最大活动块

tot-blocks-allocd 分配总数(营业额)

这样可以控制显示分配点的顺序。您可以选择查看具有最多实时字节数或最高总字节周转量,或最高数量的实时数据块或最高总数据块周转量的分配点。这些给程序行为有用的不同图片。例如,通过最大活动块进行排序往往会显示创建大量小对象的分配点。

需要注意的一个重要的一点是每个分配堆栈都是单独的分配点。因为默认情况下堆栈有12个帧,所以这往往会在多个分配点上传播数据。你可能想使用标记--num-callers = 4或一些这么小的数字来减少扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值