Rustdoc 性能改进

2021年1月15日 · Jynn Nelson 和 Guillaume Gomez 代表 Rustdoc 团队

大家好!@GuillaumeGomez 最近在推特上发布了关于 rustdoc 性能改进的消息,并建议我们写一篇关于此的博客文章。

这条推文收到了很多赞成写博客文章的想法,所以我们开始了!

性能变化

实际上只有两个 PR 是明确为了提高 rustdoc 的性能而设计的

  1. Rustdoc:缓存已解析的链接 #77700

    正如标题所说的那样。特别是,这使得生成 stm32h7xx 的内部文档链接的时间加快了惊人的 90,000%@bugadani 在这方面做得非常出色,祝贺你!

  1. 不要在内部文档链接中查找 blanket impl #79682

    这个 PR 的编写过程非常令人失望。要点是,如果你有

    trait Trait {
        fn f() {}
    }
    
    impl<T> Trait for T {}
    

    那么链接到 usize::f 不仅无法工作,而且运行时间会比其余的内部文档链接运行时间更长。这暂时禁用了 blanket impl,直到错误被修复并且性能得到改善,在 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 传递

此清理工作的第一部分也是最重要的一部分是一个名为“添加 Item::from_def_id_and_kind 以减少 rustdoc 中的重复”的 PR (#77820)。在该更改之前,rustdoc 中的每个 Item 都在数十个不同的地方构建 - 对于结构体,对于枚举,对于 trait,列表一直在继续。这使得很难更改 Item 结构体,因为任何更改都会破坏数十个调用点,每个调用点都必须单独修复。#77820 所做的是在同一个地方构建所有这些项,这使得更改 Item 在内部的表示方式变得容易得多。

在此过程中,@jyn514 发现编译器中首先需要进行一些清理

  • 在解析中计算一次可见性 #78077。感谢 @petrochenkov 解决这个问题!
  • 修复 HIR 的项名称的处理 #78345

删除 Item 的部分

完成此操作后,我们能够通过按需计算信息(使用编译器内部组件)来删除 Item 类型的大部分。这有两个好处

  1. 减少内存使用,因为信息存储的时间不会超过需要的时间。
  2. 整体上减少了时间,因为并非每个项都需要所有可用的信息。

这得益于 查询系统,我强烈建议阅读有关它的信息。

以下是一些按需计算信息的更改示例

  • 不要不必要地覆盖模块的属性 #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 中传递符号而不是标识符 #79623

正如您所看到的,所有这些更改不仅加快了 rustdoc 的速度,而且还发现了多年来一直存在的错误和重复。

重用编译器类型

以下是一些在不添加我们自己的类型的情况下使用现有编译器类型的示例

  • [rustdoc] 将 item.name 切换为符号 #80044
  • 在 rustdoc 中使用更多符号 #80047
  • 尽可能用符号替换字符串 #80091
  • 继续在 rustdoc 中将字符串转换为符号 (1) #80119
  • 继续在 rustdoc 中将字符串转换为符号 (2) #80154
  • 删除 rustdoc 中的自定义漂亮打印 #80799

它们将用于项名称的 String 替换为使用 Symbol。符号是实习字符串,所以我们不仅防止了不必要的转换,还大大提高了内存使用率。您可以在 rustc-dev-guide 中阅读有关符号的更多信息。

有趣的部分是,它还允许对编译器本身进行一些 小改进

使用相同的逻辑,出现了 #80261(这需要事先完成 #80295),它使用“转换信息”保留了原始文档属性 Symbol,而不是转换后的字符串。如果您想了解更多关于 rustdoc 如何处理文档注释格式的信息,@GuillaumeGomez这里 写了一篇关于此的博客文章。这里的想法是再次“按需”计算此结果,而不是提前存储结果以供(潜在)使用。

为什么我们之前没有更多地依赖 rustc 内部组件?

现在,你可能想知道为什么 rustdoc 在这次清理之前没有更多地依赖 rustc 内部机制。答案其实很简单:rustdoc 是老旧的。在它被编写的时候,rustc 的内部机制变化非常频繁(甚至每天都在变),这使得 rustdoc 的维护者很难跟上。为了让他们能够在不太多担心这些变化的情况下工作,他们决定抽象出编译器内部机制,以便他们可以使用这些 rustdoc 类型进行工作,而不必每天都担心出现破坏性更改。

此后,情况得到了改善,Rust 的 1.0 版本终于发布,变化速度放缓。然后,重点主要放在添加新功能上,以使 rustdoc 尽可能出色。随着新的 rustdoc 团队成员的加入,我们终于能够重新关注这方面。保留所有这些抽象已经没有太大意义了,因为内部机制现在相对稳定,我们可以看到结果。:)

下一步

正如你从展示的基准测试中看到的那样,结果非常积极。但是,我们离完成还很远。在我们说话的时候,我们仍在继续简化和重构许多 rustdoc 源代码。

完全移除 doctree

这是 doctree 今天所做的“有用的工作”(而不是不必要的复杂性)

  • 检测哪些项是公开可访问的。理想情况下,这应该只使用编译器 API,但是这些 API 已损坏

  • 内联仅可从导出访问的项。“内联”是在重新导出处(pub use std::process::Command)显示项的完整文档,而不是只显示 use 语句。标准库和 facade crates(如 futures)广泛使用它,以便在一个地方显示相关文档,而不是分散在许多 crate 中。@jyn514 希望可以在 clean 中完成此操作,但目前还不知道如何操作。

  • 将宏从始终位于 crate 的根目录移动到它们可访问的模块中。例如,这个宏

    #![crate_name="my_crate"]
    #![feature(decl_macro)]
    mod inner {
        pub macro m() {}
    }
    

    应该在 my_crate::inner::m 中记录,但是编译器在 my_crate::m 中显示它。对此的修复是一个糟糕的 hack,它会遍历 Rustdoc 知道的每个模块,以查看模块的名称是否与宏的父模块的名称匹配。在未来的某个时候,修复编译器 API 以便不再需要这样做会很好。

    非常感谢 @danielhenrymantilla,他不仅编写了修复程序,还发现了并修复了沿途的其他几个与宏相关的错误!

如果所有这些问题都可以解决,那将会是一个更大的加速——根本不需要首先遍历树!

继续缩小 clean::Item

现有的大部分清理工作都集中在按需计算 rustdoc 中每个项使用的信息,因为这具有最大的影响。但是,仍然有许多其他部分是提前计算的:特别是 ItemKind 在开始渲染文档之前会完全经过 clean

加速 collect_blanket_impls

rustdoc 中最慢的函数之一是一个名为 get_auto_trait_and_blanket_impls 的函数。在具有许多 blanket 实现的 crate 上,例如 stm32 生成的 crate,这可能会花费 rustdoc 在 crate 上花费的时间近一半

我们还不确定如何加速这个过程,但是肯定还有很大的改进空间。如果你有兴趣研究这个,请在 Zulip 上联系我们。

总的来说,rustdoc 在性能方面取得了快速进展,但是仍然有许多工作要做。

勘误

该博客文章的早期版本将有关精简 doctree 的部分描述为“烧毁树木”。名称已更改为更环保的名称。