大家好! @GuillaumeGomez 最近发了一条关于 rustdoc 性能改进的推文,并建议我们写一篇博文来介绍它
将现在 @rustlang rustdoc 与 4 个月前的 rustdoc 进行性能比较,简直太疯狂了!rustdoc 清理工作(我们离完成还远着呢!)正在产生巨大的积极影响!迫不及待地想看看结果会好多少。
— Guillaume Gomez (@imperioworld_) 2021 年 1 月 13 日
也许我应该写一篇博文? pic.twitter.com/XapdmdZ1IZ
这条推文收到了很多赞同博文想法的评论,所以我们开始吧!
性能变化
实际上只有两个 PR 是专门为了提高 rustdoc 性能而提交的
-
Rustdoc:缓存已解析的链接 #77700
这项工作正如标题所示。特别是,它将为
stm32h7xx
生成文档内部链接(intra-doc links)的时间大幅加速了惊人的 90,000%。 @bugadani 在这方面做得非常出色,恭喜!
-
不在文档内部链接中查找 blanket impls #79682
这个 PR 写起来很让人沮丧。关键是,如果你有
那么链接到
usize::f
不仅不起作用,而且运行所需的时间会比其他文档内部链接更长。这项改动暂时禁用了 blanket impls,直到 bug 修复并且性能得到改进,这使得stm32h7xx
的速度同样提升了 90 倍。你可能想知道为什么
stm32h7xx
之前如此慢;详细信息请参阅博文末尾。
全是清理工作的功劳
随着 rustdoc 团队最近的发展壮大,我们终于有时间来偿还一段时间以来积累的技术债务了。总而言之:移除 rustdoc 中的自定义实现,直接使用编译器类型。首先,我们需要稍微解释一下 rustdoc 是如何工作的。当运行它生成 HTML 文档时,它会经历几个步骤
- 运行编译器的一部分,以获取所需的信息。
- 移除编译器提供的我们不需要的信息(例如,如果一个条目是
doc(hidden)
,我们就不需要它)。这部分有很多内容可以说,所以也许我们会另写一篇博文来详细介绍。 doctree
阶段,它在编译器的某些条目上添加 rustdoc 所需的一些额外信息。clean
阶段,它将编译器类型转换为 rustdoc 类型:基本上,它将所有内容转换为“可打印”的内容。render
阶段,然后生成所需的输出(HTML,或者在 nightly 版本中是 JSON)。
@jyn514 不久前注意到 Rustdoc 中的大部分工作都是重复的:实际上存在 三 个不同的抽象语法树(AST)!一个用于 doctree
,一个用于 clean
,还有一个是编译器使用的原始 HIR。Rustdoc 花费了大量时间在它们之间进行转换。大部分的速度提升都来自于彻底消除部分 AST。
修剪树结构
doctree
所做的大部分工作都是 100% 不必要的。它拥有的所有信息都已经存在于 HIR 中,而递归遍历 crate 并构建新类型需要花费相当长的时间。
@jyn514 第一次尝试是 彻底移除这个阶段。结果...糟透了。事实证明它毕竟还是做了一些有用的工作。
话虽如此,它确实做了一些不必要的工作,那就是为所有东西添加自己的类型。如果你比较一下 3 个月前的类型 和 今天的类型,你会发现差异真的令人吃惊!它从复制代码中几乎所有编译器类型的 300 行代码减少到只有 75 行和 6 种类型。
clean
阶段
清理 这次清理工作的第一个也是最重要的部分是一个 PR,名为“在 rustdoc 中添加 Item::from_def_id_and_kind
以减少重复”(#77820)。在该更改之前,rustdoc 中的每个 Item
都在几十个不同的地方构建——对于结构体、枚举、trait 等等不一而足。这使得对 Item
结构体进行更改变得非常困难,因为任何更改都会破坏几十个调用点,每个调用点都必须单独修复。#77820 的作用是将所有这些条目构建在同一个地方,这使得更改 Item
在内部的表示方式变得容易得多。
在此过程中,@jyn514 发现了一些首先需要在编译器中进行的清理工作
- 在 resolve 阶段一次性计算可见性 #78077。感谢 @petrochenkov 解决了这个问题!
- 修复 HIR 条目名称的处理 #78345
Item
的部分内容
删除 完成之后,我们能够通过按需计算信息(而不是预先计算)并使用编译器内部机制来移除 Item
类型的大部分内容。这带来了两个好处
- 内存使用更少,因为信息不再需要时就不会一直存储。
- 总体时间更少,因为并非每个条目都需要所有可用的信息。
这很大程度上受益于 查询系统,我强烈建议大家阅读相关内容。
以下是一些按需计算信息的示例更改
- 不要不必要地覆盖 Module 的 attrs #80340
- 移除
clean::Deprecation
#80041 - 移除
clean::{Method, TyMethod}
#79125 - 移除重复的
Trait::auto
字段 #79126 - 移除一些 doctree 条目 #79264
- 移除
doctree::{ExternalCrate, ForeignItem, Trait, Function}
#79335 - 移除
doctree::Impl
79312 - 移除
doctree::Macro
并区分macro_rules!
和pub macro
#79455 - 在 doctree 中传递 Symbols 而非 Idents #79623
正如你所见,所有这些更改不仅加速了 rustdoc,还发现了存在多年的 bug 和重复代码。
复用编译器类型
以及一些使用现有编译器类型而不是添加自定义类型的示例
- [rustdoc] 将
item.name
切换到 Symbol #80044 - 在 rustdoc 中使用更多 symbol #80047
- 尽可能将 String 替换为 Symbol #80091
- 继续在 rustdoc 中进行 String 到 Symbol 的转换 (1) #80119
- 继续在 rustdoc 中进行 String 到 Symbol 的转换 (2) #80154
- 移除 rustdoc 中的自定义 pretty-printing #80799
它们将用于条目名称的 String
替换为使用 Symbol
。Symbol 是 interned strings,因此我们不仅避免了不必要的转换,还极大地改善了内存使用。你可以在 rustc-dev-guide 中阅读更多关于 Symbol 的信息。
有趣的是,这也使得编译器本身获得了一些 小改进。
按照同样的逻辑,#80261(需要先有 #80295)被引入,它保留了原始文档属性 Symbol
及其“转换信息”,而不是存储转换后的字符串。如果你想了解更多关于 rustdoc 如何处理文档注释格式的信息,@GuillaumeGomez 在这里写了一篇博文。这里的想法再次是“按需”计算这些信息,而不是提前存储结果以备(潜在)使用。
为什么我们之前没有更多地依赖 rustc 内部机制?
现在,你可能想知道为什么 rustdoc 在这次清理之前没有更多地依赖 rustc 内部机制。答案其实很简单:rustdoc 很老了。在它编写时,rustc 内部机制变化非常频繁(甚至每天都在变),这使得 rustdoc 维护者很难跟上。为了让他们工作时不过多担心这些变化,他们决定对编译器内部机制进行抽象,这样他们就可以使用那些 rustdoc 类型进行工作,而不必担心每天都有破坏性的更改。
从那时起,情况得到了改善,Rust 1.0 版本终于发布,事情也稳定下来了。然后,重点主要放在添加新功能上,以使 rustdoc 尽可能地出色。随着新的 rustdoc 团队成员的加入,我们终于能够重新关注这方面的工作。保留所有这些抽象意义不大,因为内部机制现在相对稳定了,而且我们都能看到成果。:)
后续步骤
正如你从显示的基准测试中看到的,结果是非常积极的。然而,我们离完成还远着呢。在我们交谈的同时,我们仍在继续简化和重构大量的 rustdoc 源代码。
彻底移除 doctree
这是 doctree
今天所做的“有用的工作”(与不必要的复杂性相对)
-
检测哪些条目是公开可达的。理想情况下,这应该直接使用编译器 API,但那些 API 有问题。
-
内联仅通过导出(export)可达的条目。“内联”是指在重新导出(re-export)(
pub use std::process::Command
)的地方显示条目的完整文档,而不是只显示use
语句。标准库和像futures
这样的 facade crates 普遍使用它来将相关文档显示在一个地方,而不是分散在许多 crate 中。@jyn514 希望这可以在clean
阶段完成,但尚不清楚如何实现。 -
将宏从始终位于 crate 的根目录移动到它们可访问的模块中。例如,这个宏
应该在
my_crate::inner::m
文档化,但编译器却将其显示在my_crate::m
。这个问题的修复是一个糟糕的 hack,它遍历 Rustdoc 已知的每个模块,查看模块的名称是否与宏的父模块的名称匹配。将来某个时候,如果能修复编译器 API 以使其不再必要,那将太好了。非常感谢 @danielhenrymantilla,他不仅编写了修复方案,还在过程中发现并修复了其他几个与宏相关的 bug!
如果所有这些问题都能得到解决,那将带来更大的速度提升——因为一开始就完全不需要遍历树结构了!
clean::Item
继续精简 现有的大部分清理工作都集中在按需计算 rustdoc 中 每个 条目都需要的信息,因为这影响最大。但是,仍然有很多其他部分是提前计算的:特别是 ItemKind
在开始渲染文档之前会完全经过 clean
阶段。
collect_blanket_impls
加速 在整个 rustdoc 中,最慢的函数之一是名为 get_auto_trait_and_blanket_impls
的函数。对于包含许多 blanket implementation 的 crate(例如 stm32
生成的 crate),这可能会占用 rustdoc 处理该 crate 总时间的近一半。
我们还不确定如何加速它,但肯定有很大的改进空间。如果你对此感兴趣并想参与工作,请在 Zulip 上联系我们。
总的来说,rustdoc 在性能方面取得了快速进展,但仍有很多工作要做。
勘误
博文的早期版本将关于精简 doctree
的部分描述为“烧毁树结构”。这个名称后来改为更加环保的说法。