黑线鳕是文档生成Haskell的工具。给定一组Haskell模块,Haddock可以生成这些模块的各种格式文档基于中特殊格式的注释源代码。Haddock是Haskell生态系统的重要组成部分Haskell软件的作者和用户依靠Haddock生成的文档来熟悉代码。

最近,水银要求我们调查性能Haddock中的问题阻碍了开发人员使用它有效。特别是,当运行Mercury的代码时,Haddock会吃光64GB机器上的所有内存。这给他们的开发人员带来了困难生成文档以在本地浏览。

从更高的层面上来说,这篇文章所涵盖的工作已经让Haddock记忆犹新用法粗略减半.Haddock和GHC全套变更这些改进将与GHC 9.8一起发货。

所有这些分析和调试工作都是使用事件日志2htmlghc调试工具,它们是非常适合描述和诊断Haskell程序的性能。然而,这些人的知名度远远低于他们应得的旨在作为一个案例研究,展示我们如何使用这些工具对复杂的实际应用程序的显著性能改进。

如果您有兴趣了解有关这些工具的更多信息,请继续阅读或查看我们的一些视频:

描述问题的特征

诊断Haddock内存行为的第一步是生成一个内存配置文件事件日志2html.

事件记录是可以由GHC运行时生成的文件当Haskell程序被执行时的系统,其中包含许多关于对分析有用的执行。当程序使用堆运行时启用了评测,它将定期向事件日志发送堆使用示例。随后,事件日志2html可用于将此文件转换为HTML具有交互式可视化和程序内存统计信息的文档轮廓。浏览由生成的配置文件事件日志2html经常导致发现其他方面很难找到的优化机会。

以下基线配置文件显示了Haddock使用的实时堆数据在上生成HTML文档阿格达基本代码:1

事件日志2html区域图,基线

您还可以看到由生成的完整HTML页面事件日志2html对于基线,和更高版本的配置文件还附带指向HTML版本的链接。

注:Chrome可能无法呈现配置文件的“详细”选项卡(请参阅事件日志2html问题166). Firefox浏览器建议使用。

这表明内存使用量在运行过程中会增加,直到达到Haskell堆上1.1GB实时数据的峰值。因为这只算活数据,实际的进程内存使用量会高得多(例如,复制垃圾收集器至少使所需内存量加倍)。“堆”选项卡事件日志2html输出显示了更多细节:它包括“Live Bytes”(如主配置文件)和“堆大小”(运行时系统使用的总内存),在本例中达到2.3GB。

信息表评测

此配置文件是使用一种新的配置方法info table profiling生成的这样就避免了在分析模式下重新编译应用程序的需要提供了额外的细节。你可以阅读有关信息表评测的更多信息在我们之前的帖子中。这意味着事件日志2html“详细”标签也很有启发性(此处显示完整的HTML页面):

事件日志2html详细图表,基线

信息表大致对应于分配站点(例如thunk或数据构造函数应用程序)。对于每个信息表,我们看到一个随时间推移堆使用情况的迷你图配置文件,以及类型信息和显示负责分配的代码的源位置。这使我们能够更详细地探讨过度分配的来源。

提供给Haddock的任何源模块都首先使用GHC API进行类型检查。因此,我们需要仔细区分该简介来自GHC,而不是Haddock本身。深入了解细节后发现概要文件开始时内存使用的明显峰值来自GHC类型检查器。可以通过查找导致该峰值的闭包(堆对象),其中大多数是列表闭包在中分配全球总部。Tc单位。Zonk(分区)模块(仅用于类型检查)。

使用查找固定器ghc调试

表的第三行显示堆的大部分被GRE(希腊)的构造函数GlobalRdrElt公司类型。这些值表示“全局重命名环境”,用于跟踪事物名称解析期间在范围内的。2知道Haddock是如何操作的,这些闭包没有充分的理由在整个跑步过程中一直呆在堆上。相反,一旦模块由Haddock处理后,该模块的作用域信息不再保留在内存中。

为了找出这些闭包在堆上堆积的原因,我们可以使用ghc调试的保持器分析功能。ghc调试是一种工具通过以下方式以交互或编程方式检查Haskell进程的堆暂停并附加调试器进程。要了解更多信息,您可以查看我们的关于使用ghc-debug分析内存碎片的前一篇文章.

在用Haddockghc调试我们可以连接到进程使用ghc-调试块,其中提供了一个终端UI,用于在堆。特别是,它可以搜索GRE(希腊)闭合,这会产生如下的保持器路径:

GRE闭包的ghc-debug保持器配置文件

这个GRE(希腊)我们正在检查的闭包显示在最上面。项目在它下面标记为字段的是闭包GRE(希腊)closure正在保留(保留对的引用)。的固定器路径GRE(希腊)显示闭包在田野下面。路径中显示的每个闭包都由它下面的闭包(尽管这些路径不一定是唯一的,所以它也可以在其他地方引用)。

在这种情况下,我们可以从编号为15的护圈中看到GRE(希腊)闭包由ModI面closure,我们将其展开以显示其字段。通过比较闭包地址,我们可以看到引用在字段12中,全局(_G)的字段ModI面构造函数。

虽然它需要一些关于Haskell堆和程序的预期内存行为,ghc调试成功了可以非常精确地跟踪问题。

解决问题

作为这个文档全局(_G)领域表明,它实际上仅由GHCi使用。然而,它在某些时候也为GHC的“无代码”后端启用,这是Haddock使用什么。结果我们可以将此字段设置为什么都没有用于GHC的无代码后端和将Haddock的内存使用量减少约20%,且没有任何负面影响。请参见!10469用于修补程序。

只有GHC的这一变化,而没有变化Haddock在Agda代码库上的最高实时数据驻留率现已降至950MB以下,具有以下配置文件(满的事件日志2html此处输出):

消除GRE闭包后的事件日志2html区域图

再也没有了GRE(希腊)在“Detailed”选项卡中控制概要文件的闭包:

事件日志2html详细图表,消除GRE关闭

以简单的方式减少内存使用

回答有关程序内存使用的难题通常会导致优化机会。当我们基本上回答“这些是什么”的问题GRE(希腊)建筑工人在这里干什么?”相反,回答简单的问题有时也同样有效。这尤其适用于Haddock,因为它已经开发了15年多,有很多灰尘结果导致其源代码中出现角点。

本质上,Haddock是一个将Haskell源代码转换为中间表示法然后转换为某些文档格式(例如HTML或LaTeX)。Haddock使用数据结构作为其中间表示接口类型.Haddock的核心功能是生成接口值来自使用GHC API的Haskell模块,然后从这些模块生成文档接口值。

通过接口键入并回答简单的问题“这个字段用于什么?”导致了许多优化。甚至还有接口正在定义的类型(和如此分配),但从未使用过。这些字段可以简单地删除总共。

这个过程还产生了一个不那么简单的优化。Haddock能够生成可由接收和索引的文本格式胡格尔Haskell API搜索引擎。什么之中的一个的字段接口类型(ifaceRnExportItems)只用于生成这些Hoogle文件,即使Hoogle未请求输出。此外,通过此保留的thunks对于他们用来计算的结果,字段过大。相反,我们可以急切地生成所需的值请求Hoogle输出时。

这些变化导致Haddock的内存使用量再次急剧下降。用打了补丁的GHC和Haddock,Agda基准测试上的峰值实时数据驻留容量现在刚刚超过550MB,个人资料看起来像这个(满的事件日志2html此处输出):

常规内存修复后的事件日志2html区域图

嗨,哈多克!

Haddock最初设计用于使用GHC对源模块进行类型检查API,然后直接从类型检查的结果生成其中间表示。这已经两个明显的缺点:

  • 即使程序已经编译好,也必须再次进行类型检查生成文档(由于需要执行Template Haskell拼接,重新检查类型通常意味着完全重新编译)。

  • 正如我们所看到的,类型检查的结果包含了更多的信息而不仅仅是生成文档所必需的。

长期以来的“嗨,哈多克”努力通过从根本上改变来解决这一问题Haddock的工作方式。而不是根据类型检查,Haddock现在使用接口文件(你好文件)由GHC在编译过程中生成,以描述每个模块的接口。

这修复了上面列出的两个问题:

  • 一旦程序编译完成,Haddock所需的信息已经出现在你好文件,以后可以重用。此外,现有重新编译避免机制可用于决定后续当源文件发生更改时,需要进行编译。

  • 存储在中的文档结构你好文件可以是最小数量Haddock的要求,避免了在内存。

Hi Haddock项目最初是一项重大工程由Simon发起2018年雅各比Alec Theriault、Hécate Moonlight和祖宾达加尔。作为我们减少Haddock内存使用的工作的一部分,Well-Typed完成并登陆了Haddock的补丁(哈多克MR1597). 我们感谢所有人多年来,他为Hi Haddock的工作做出了贡献。

性能影响非常好。在顶部添加Hi Haddock更改上述补丁导致峰值实时数据驻留仅为380MB,具有以下配置文件(满的事件日志2html输出在这里):

带有Hi Haddock更改的eventlog2html区域图

此外,当可以避免重新编译时,因为最新的编译结果已经可用,内存使用稍低,但时间显著下降(满的事件日志2html输出在这里):

避免重新编译时的事件日志2html区域图

结论和未来工作

由于这项工作,Haddock在以下情况下将使用更少的内存生成文档。因此,实现了许多这些改进使用分析Haddock事件日志2htmlghc调试在这些改进的基础上,我们实现了Hi Haddock的改变这导致了额外的性能提升。

此博客帖子中显示的结果是在Haddock运行时捕获的它的默认HTML-only模式,因为这项工作主要集中在减少Haddock的基线内存使用量。Haddock有几种后端和模式可能成熟的操作,在空间和时间上都有更多的机会优化。

此外,用户还需要做更多的工作才能充分利用Hi Haddock的优势。它是目前,在文档生成过程中很难避免重新编译当通过Cabal执行Haddock时,Cabal将几个标志传递给Haddock这总是使以前的构建结果无效。我们目前正在实施对Cabal和Haddock进行必要的更改以平滑此界面(请参见电缆MR 9177哈多克MR 38).

我们非常感谢水银为了让这件事成功尽可能在Haddock上运行,并向所有Haddock维护者和贡献者致敬。谢谢也请向David Christiansen反馈此博客帖子的草稿。

Well-Typed能够在GHC、HLS、Cabal、Haddock和其他方面工作核心Haskell基础设施得益于来自各方面的资金赞助商。如果你的公司能够为这项工作做出贡献,赞助商请提供维护工作或其他功能的实施资金阅读关于如何提供帮助当选触摸.


  1. Agda代码库被选为这项工作的样本,因为它是一个足够大的项目,Haddock的执行阶段在内存配置文件,它使用一组不同的Haskell语言特性应能显著覆盖Haddock的代码路径。↩︎

  2. 请参见此GHC wiki页面了解更多信息关于重命名器的信息.↩︎