哈多克 是文档生成 Haskell的工具。 给定一组Haskell模块,Haddock可以生成 这些模块的各种格式文档基于 中特殊格式的注释 源代码。 Haddock是Haskell生态系统的重要组成部分 Haskell软件的作者和用户依靠Haddock生成的文档来 熟悉代码。
最近, 水银 要求我们调查性能 Haddock中的问题阻碍了开发人员使用它 有效。 特别是,当运行Mercury的代码时,Haddock会吃光 64GB机器上的所有内存。 这给他们的开发人员带来了困难 生成文档以在本地浏览。
从更高的层面上来说,这篇文章所涵盖的工作已经让Haddock记忆犹新 用法粗略 减半 .Haddock和GHC全套变更 这些改进将与GHC 9.8一起发货。
所有这些分析和调试工作都是使用 事件日志2html
和 ghc调试
工具,它们是 非常适合描述和诊断Haskell程序性能。 然而,这些人的知名度远远低于他们应得的 旨在作为一个案例研究,展示我们如何使用这些工具 对复杂的实际应用程序的显著性能改进。
如果您有兴趣了解有关这些工具的更多信息,请继续阅读或查看 我们的一些视频:
描述问题的特征
诊断Haddock内存行为的第一步是生成一个 内存配置文件 事件日志2html
.
安 事件记录 是可以由GHC运行时生成的文件 执行Haskell程序时,包含有关 对分析有用的执行。 当程序使用堆运行时 启用了评测,它将定期向事件日志发送堆使用示例。 随后, 事件日志2html
可用于将此文件转换为HTML 具有交互式可视化和程序内存统计信息的文档 轮廓。 浏览由生成的配置文件 事件日志2html
经常导致 发现其他方面很难找到的优化机会。
以下基线配置文件显示了Haddock使用的实时堆数据 在上生成HTML文档 阿格达 基本代码:
事件日志2html区域图,基线
您还可以看到 由生成的完整HTML页面 事件日志2html
对于基线 , 和更高版本的配置文件还附带指向HTML版本的链接。
注: Chrome可能无法呈现配置文件的“详细”选项卡(请参阅 事件日志2html问题166 ). Firefox浏览器 建议使用。
这表明内存使用量在运行期间增加,直到达到 Haskell堆上1.1GB实时数据的峰值。 因为这只算活 数据,实际的进程内存使用量会高得多(例如,复制 垃圾收集器至少使所需内存量加倍)。 “堆”选项卡 的 事件日志2html
输出显示了更多细节:它包括“Live Bytes”(如 主配置文件)和“堆大小”(运行时系统使用的总内存), 在本例中达到2.3GB。
信息表评测
此配置文件是使用一种新的配置方法info table profiling生成的 这样就避免了在分析模式下重新编译应用程序的需要 提供了额外的细节。 你可以 阅读有关信息表评测的更多信息 在我们之前的帖子中。 这意味着 事件日志2html
“详细”标签也很有启发性 ( 此处显示完整的HTML页面 ):
eventlog2html详细信息图表,基线
信息表大致对应于分配站点(例如thunk或数据 构造函数应用程序)。 对于每个信息表,我们看到一个 随时间推移堆使用情况的迷你图配置文件,以及类型信息和 显示负责分配的代码的源位置。 这使我们能够 更详细地探讨过度分配的来源。
提供给Haddock的任何源模块都首先使用GHC API进行类型检查。 因此,我们需要仔细区分 配置文件来自GHC,而不是Haddock本身。 深入了解细节后发现 概要文件开始时内存使用的明显峰值来自GHC 类型检查器。 可以通过查找 导致该峰值的闭包(堆对象),其中大多数是列表闭包 在中分配 全球总部。 Tc.实用程序。 Zonk(分区)
模块(仅用于类型 检查)。
使用查找固定器 ghc调试
表的第三行显示堆的大部分被 GRE(希腊)
的构造函数 GlobalRdrElt公司
类型。 这些值表示“全局重命名 环境”,用于跟踪事物 名称解析期间在范围内的。 知道Haddock是如何操作的,这些闭包没有充分的理由 在整个跑步过程中都要坚持在堆上。 相反,一旦模块 由Haddock处理后,该模块的作用域信息不再保留在内存中。
为了找出这些闭包在堆上堆积的原因,我们可以使用 ghc调试
的保持器分析功能。 ghc调试
是一种工具 通过以下方式以交互或编程方式检查Haskell进程的堆 暂停并附加调试器进程。 要了解更多信息,您可以查看我们的 关于使用ghc-debug分析内存碎片的前一篇文章 .
在使用以下命令检测Haddock之后 ghc调试
我们可以连接到 进程使用 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 未请求输出。 此外,这个 对于他们用来计算的结果,字段过大。 相反,我们可以急切地生成所需的值 请求Hoogle输出时。
这些变化导致哈多克的内存使用量再次大幅下降。 用打了补丁的GHC和Haddock, Agda基准测试上的峰值实时数据驻留容量现在刚刚超过550MB, 个人资料看起来像 这个( 满的 事件日志2html
此处输出 ):
常规内存修复后的事件日志2html区域图
嗨,哈多克!
Haddock最初设计用于使用GHC对源模块进行类型检查 API,然后直接从类型检查的结果生成其中间表示。 这已经 两个明显的缺点:
长期以来的“Hi Haddock”努力从根本上改变了这一点 Haddock的工作方式。 而不是根据 类型检查,Haddock现在使用 接口文件( 你好
文件)由GHC在编译过程中生成,以描述 每个模块的接口。
这修复了上面列出的两个问题:
Hi Haddock项目最初是一项重大工程 由Simon发起 2018年雅各比 和 Alec Theriault、Hécate Moonlight和 祖宾 达加尔 。作为我们减少Haddock内存使用的工作的一部分, Well-Typed完成并登陆了Haddock的补丁( 哈多克MR 1597 ). 我们感谢所有人 多年来,他为Hi Haddock的工作做出了贡献。
性能影响非常好。 在顶部添加Hi Haddock更改 上述补丁导致峰值实时数据驻留仅为380MB, 具有以下配置文件( 满的 事件日志2html
输出 在这里 ):
带有Hi Haddock更改的eventlog2html区域图
此外,当可以避免重新编译时,因为最新的编译 结果已经可用,内存使用稍低,但时间 明显下降( 满的 事件日志2html
输出 在这里 ):
避免重新编译时的事件日志2html区域图
结论和未来工作
由于这项工作,Haddock在以下情况下将使用更少的内存 生成文档。 因此,实现了许多这些改进 使用分析Haddock 事件日志2html
和 ghc调试
在这些改进的基础上,我们实现了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基础设施得益于来自各方面的资金 赞助商。 如果您的公司能够为这项工作做出贡献,请赞助 请提供维护工作或其他功能的实施资金 阅读 关于如何提供帮助 或 当选 触摸 .