在这篇文章中,我们将使用一个全新的(在写作时)GHC中的某种实验性分析方法,以显示如何识别内存这个新的分析方法,由马修,允许我们将堆闭包映射到源位置。的一个关键功能这种新的剖析模式不需要剖析构建(即。带有的建筑物-教授). 这是可取的,因为非profiled构建使用更少并且往往更快,主要是通过避免簿记阻止优化机会。

非常感谢你哈苏拉对于与我们合作并使这里介绍的工作成为可能。

让我们直接进入并尝试分析内存泄漏。虽然你不需要阅读为了关注这篇博客文章,我们将使用以下代码:大Thunk.hs.博客文章的其余部分将展示如何识别代码中的问题无需提前阅读。

现状

让我们构建/运行大Thunk.hs使用标准,-小时,中已有的内存配置文件全球总部。我们会用事件日志2html.请注意,这不是一个配置文件(-教授)LargeThunk可执行文件的构建:

$ghc公司-事件日志 -rtsopts公司 -氧气大Thunk
$./LargeThunk 100000 100000 30000000+RTS-我 -小时 -i0.5级 -RTS系统
$事件日志2html大型Thunk.eventlog

这个-事件日志-rtsopts选项允许我们使用-我运行时系统选项,其中生成大型Thunk.eventlog文件。-氧气打开优化。这个-i0.5级RTS选项强制每0.5秒执行一次堆配置文件,并且-小时表示堆配置文件应按闭包类型细分。这个100000 100000 30000000是导致数GB的LargeThunk程序的参数内存使用率。最后,事件日志2html将事件日志呈现为方便的html文件:LargeThunk.eventlog.html.

你可以看到THUNK公司s(未评估的闭包)从11秒开始到54秒左右。尽管使用了各种!s和NFData公司/试图避免这样THUNK公司s.我们已经确定了可能的内存泄漏,但我们不知道什么是代码导致的。在更大的应用程序,识别违规代码很困难这会让我们抓耳挠腮。

我们的新剖析方法

我们的新分析方法是三个新标志的组合:-你好运行时系统标志和-查找表映射-fdistict-constructor表编译时标志。

这些选项都与信息表相关。堆由“闭包”组成其中包括评估的Haskell值和未评估的thunk值。每个堆上的闭包包含指向info表的指针,该表是关于闭包的类型和内存布局。许多闭包可能引用相同的信息表,例如在同一源位置创建的thunks。评价的相同类型和相同数据构造函数的闭包也将指向相同的信息表,但我们会看到这将随着-fdistict-constructor表.

当运行时系统执行内存配置文件时各种选项,运行时系统偶尔会扫描堆上的所有活动闭包。测量每个封口的尺寸,并根据使用的分析选项。例如,在上一节中-人T选项按每个闭包的“闭包类型”分解概要文件。新的-你好选项只需按每个闭包的信息表指针。很快就会明白为什么这是有用的。

使用编译时-查找表映射选项,GHC将根据info表指针指向源位置和一些额外的类型信息。我们打电话给该映射的每个条目都是“信息表出处条目”或简称IPE。此映射将烘焙为结果二进制文件。当然,这增加了二进制大小。给定闭包的信息表指针(例如,从-你好profile),我们可以使用映射来获取闭包所在的源位置已创建。额外的类型信息意味着我们不仅知道thunks是thunk,但我们也知道thunk被评估后的类型,例如双精度字符串。此源位置和类型信息对于调试来说是非常宝贵的以及使用这种新的剖析方法的关键优势。

使用-你好-查找表映射我们将为thunks获取有用的源位置但不适用于已评估的闭包。因此-fdistict-constructor表编译时选项,根据创建不同的信息表使用数据的建造师。这进一步增加了二进制文件的大小,但会产生有用的源代码对调试至关重要的已评估闭包的位置。

尝试一下

现在让我们用大Thunk.hs例子:

$温室气体-事件日志 -rtsopts公司 -氧气 -查找表映射 -fdistict-constructor表大Thunk
$./LargeThunk 100000 100000 30000000+RTS-我 -你好 -i0.5级 -RTS系统
$事件日志2html LargeThunk.eventlog

这将生成:LargeThunk.eventlog.html.

毫不奇怪,该图看起来与我们的-小时配置文件,但我们的修改版本事件日志2html在中生成了“详细”选项卡LargeThunk.eventlog.html。此选项卡允许我们将配置文件视为交互式表格:

我们希望在这个表的顶部附近找到有趣的东西,因为它是按综合规模,即居住地与时间的积分。第一排对应于具有信息表地址的闭包0x70b718号。来自描述CTy公司列,我们可以知道这些闭包是:列表构造函数。具体来说,在全球总部。列表源模块位置librarys/base/GHC/List.hs:836:25如中所列模块位置柱。我们还可以从最左侧列中的迷你图中看到这些封闭的驻留在整个剖面中基本保持不变。这个并不是特别可疑,而且凭借一点领域知识,我们知道程序在启动时生成一个大列表,可以解释这一行轮廓。

接下来的三行要有趣得多。闪光灯展示了这三个闭包密切相关。闭合类型表明是THUNK和类型表示他们将双精度s一经评估。加起来,这些组成了大约3 GiB的内存。让我们看看中的相应代码LargeThunk.hs公司. The位置列指向加权分数'重量'第149行以及userTrust(用户信任)第154行:

{- 142 -}转到:: 地图 电影ID(双精度,双精度)-> 额定值 -> 地图 电影ID(双精度,双精度)
{- 143 -}过磅(额定值userID movieID分数)=M.alter公司
{- 144 -}(\案例
{- 145 -}                    没有什么 -> 只是(userTrust*分数,userTrust)
{- 146 -}                    只是(权重分数,权重)-> 
{- 147 -}加权分数'=加权得分+(用户信任*分数)
{- 148 -}重量'=重量+userTrust(用户信任)
{- 149 -}                        在里面 只是(权重分数',权重')
{- 150 -})
{- 151 -}电影ID
{- 152 -}重量
{- 153 -}                哪里
{- 154 -}userTrust(用户信任)=信任((用户movieDB)M。!用户ID)

我们现在可以看到THUNK闭包来自3个惰性绑定:加权分数',重量'、和用户信任。通过查看我们可以看出,我们正在折叠一大串评级:

foldl’go M.empty(收视率movieDB)

每次呼叫创建新的THUNK公司是以前这样的THUNK公司s.这导致了一长串桑克斯导致内存泄漏。注意,尽管foldl(折叠)蓄能器和累加器是一个严格的映射,映射的元素只计算为弱head范式。在这种情况下,元素是元组,其内容是懒惰。通过添加!s、 我们对3个绑定进行了严格限制,避免了THUNK,从而避免内存泄漏:

{- 142 -}转到:: 地图 电影ID(双精度,双精度)-> 额定值 -> 地图 电影ID(双精度,双精度)
{- 143 -}过磅(额定值userID movieID分数)=M.alter公司
{- 144 -}(\案例
{- 145 -}                    没有什么 -> 只是(userTrust*分数,userTrust)
{- 146 -}                    只是(权重分数,权重)-> 
{- 147 -}                        !加权分数'=加权分数+(userTrust*分数)
{- 148 -}                        !重量'=重量+userTrust(用户信任)
{- 149 -}                        在里面 只是(权重分数',权重')
{- 150 -})
{- 151 -}电影ID
{- 152 -}重量
{- 153 -}                哪里
{- 154 -}                !userTrust(用户信任)=信任((用户movieDB)M。!用户ID)

如果我们运行与以前相同的配置文件,我们会看到内存泄漏已经消失!我们节省了大约3 GiB的堆驻留时间,并将运行时间:LargeThunk.eventlog.html.

结论

我们使用了新的分析方法,-你好,以直接确定源位置与懒惰导致的内存泄漏有关。我们在没有已分析版本。这是朝着调试生产方向迈出的一步很少在启用评测的情况下构建的构建。

我们中的一些人已经在使用新的分析模式来帮助分析内存问题:

  • 我们(马修大卫)已使用此方法识别小内存泄漏在GHC中。我们都快速独立地发现了相同的内存泄漏。这个基于编译Cabal时使用的配置文件。请参见#18925!4429。虽然泄漏量相对较小,这则轶事表明新的分析方法允许开发人员快速查明内存问题,即使在像GHC这样的大型代码库。

  • 马修尝试使用分析模式分析中的内存问题潘多克很快发现了一个问题由于过度懒惰,XML解析库保留了所有输入标记。

  • Zubin Duggal在他的XMonad配置中调试了内存泄漏。分析模式使他能够很快地识别出源程序中的泄漏位置正在发生。这一次不是懒惰造成的,而是名单不断增加在程序执行期间。

在撰写本文时,这项工作尚未并入GHC,但正在MR审查!3469.对于那些渴望尝试它的人,我们建议构建返回GHC 8.10.2。请参阅这些说明书因为您可能希望使用-finfo-table-map-f区分构造表并使用事件日志2html使用生成IPE支持。请尝试一下,如果您在自己的应用程序中使用分析模式成功,请告诉我们。

二进制大小

提到了二进制文件大小的增加,但它的影响有多大?阴谋集团安装例如,没有IPE的干净构建产生4440万B二元的。使用-finfo-table-map-f区分构造表对于电缆安装及其所有依赖项,包括基础产生3.188亿桶二元的。这是一个显著的增长,也是使用这种新产品时需要考虑的成本剖面法。这可以通过仅为选定代码构建IPE来缓解例如卡瓦尔图书馆和电缆安装二元的。这将产生135MiB二进制文件,但当然只有较小的闭包子集:仅包含源位置在卡瓦尔/电缆安装.

与DWARF的比较

GHC已经支持DWARF调试输出。IPE之间的主要区别基于IPE的分析和基于DWARF的分析数据(即。闭包)到源位置,同时DWARF关联机器代码到源位置。实际上,DWARF允许时间使用诸如教授(请参见的帖子位于GHC中的DWARF支持). 那可以告诉我们什么源代码负责我们程序的运行时。IPE开启另一方面,允许记忆分析,它可以告诉我们什么是源代码负责数据的分配。IPE和DWARF的优势共享的是,它们可以以最少的时间和内存性能使用间接费用。在这两种情况下,这都是以较大的二进制文件大小为代价的。