优化 GHC 构建速度
摘要
本文概述了一种系统的方法来改进 Haskell 构建时间,通过基准测试、识别瓶颈(如慢模块)、分析原因(如过度的类型级编程),并评估优化中的权衡。
<p><a href="https://lobste.rs/s/3ww0wy/optimising_for_fast_builds_with_ghc">评论</a></p>
查看缓存全文
缓存时间: 2026/09/29 05:58
# 使用 GHC 优化快速构建
**概要:** 通过基准测试、识别瓶颈(如慢速模块)、分析原因(如过度使用类型级编程)并尝试权衡方案,而非盲目应用优化,来系统性诊断 Haskell 项目的缓慢构建问题。
## 改进构建时间的系统性方法
Circuit Hub 的 Tio Carrere 分享了一套处理大型 Haskell 项目编译缓慢问题的系统性流程。随着项目规模增长,编译延迟会严重影响开发效率。将构建性能视为与其它性能指标同等重要,需要采用谨慎且迭代的方法。
推荐流程如下:
1. **基准测试**:找出构建缓慢的部分(例如最慢的 10 个模块)。
2. **建立假设**:推测这些模块缓慢的原因(例如它们都大量使用基于泛型的数据类型)。
3. **尝试验证**:测试潜在的解决方案(例如用手写实例替换泛型派生)。
4. **评估权衡**:该修复方案是否值得付出代价?或许使用 Template Haskell 或直接接受当前构建时间是更好的选择。
5. **迭代优化**:通过反复实践,加深对项目结构和技术决策的理解。
此过程不仅能加速构建,还能提升对项目整体架构及性能特性设计逻辑的理解。
## 影响构建时间的因素
构建时间取决于多个因素:
* 源代码
* GHC 编译器
* 构建工具(如 Cabal、Stack)
* 编译器及工具选项
* 机器硬件
* 环境随机性
本讨论主要关注**源代码**及其结构。
### 假设场景
讨论基于两种常见场景:
1. **本地开发**:使用标准笔记本电脑(约 16 核),采用 `cabal` 或 `stack`(非 Buck 等高级工具),禁用优化(`-O0`),并使用 HLS 或 GHCi。
2. **CI 环境**:使用 Nix(每个包一个 derivation),多个并行 CI runner,但受内存和 GHC 限制,每个包的并行度通常限制在 8 核左右。此环境会启用优化。
## 理解项目结构与并行化
Haskell 项目是包与模块的层次结构,每个依赖关系构成有向无环图(DAG)。这种结构决定了编译时的并行能力。
### 模块级编译
包的模块图决定了可并行化的编译程度。例如,一个包含六个模块(每个编译耗时 1 秒)的包,由于依赖约束,并非仅用双核就能缩短至 3 秒完成。**关键路径**——最长的依赖模块序列——即使在无限并行的情况下也决定了最小编译时间。
### 关键路径的影响
关键路径至关重要,因为:
* **并行化**:它决定了增加 CPU 核心对加速构建的理论极限。
* **缓存(避免重编译)**:最坏情况下,对关键路径上模块的修改会触发其所有传递依赖的重编译,这决定了本地开发时的最大重建时间。
对于使用 Nix 的 CI,相关的关键路径位于**包级别**,因为缓存是按包而非按模块进行的。
## 诊断慢速模块的工具与技巧
为识别瓶颈,可使用 `ghc-build-stats`、`time-ghc-modules` 以及解析 GHC 转储计时输出的工具等。
### 使用 `ghc-build-stats`
这款基于插件的工具(适用于 GHC ≥9.10)工作原理如下:
1. 在 `.cabal` 文件中将其添加为依赖和插件。
2. 运行构建,生成换行分隔的 JSON 统计文件。
3. 使用 `ghc-build-stats` 可执行文件处理这些文件,报告最慢的模块。
**重要提示:** 使用转储计时功能时,请用单核进行编译,以避免报告 CPU 时间而非实际耗时的错误。
### 第一步:质疑其必要性
识别出最慢模块后,首要问题是:**“我们是否仍需要此模块?”** 它可能是已删除功能的残留。`Weeder` 等工具可帮助查找未使用代码,但可能无法捕捉所有情况(如废弃的 API 端点)。
### 分析核心输出与二分定位
进一步分析时:
1. **检查核心输出**:使用 `-ddump-simpl` 并查看大小摘要(terms、types、coercions)。数值过大可能表明定义臃肿。
2. **模块二分定位**:注释掉一半代码或用 `undefined` 桩函数替代,以快速隔离导致编译缓慢的具体小段代码。
## 编译缓慢的常见原因与缓解措施
单一定义编译缓慢的两个主要原因是:
1. **过度使用类型级编程**:类型级编程可能具有二次方或更高的复杂度。常见问题源头包括:
* **泛型(Generics)**:泛型本身可能问题不大,但**基于泛型派生(deriving via Generics)** 常成为性能瓶颈。
* **高阶数据(Higher-Kinded Data)**:会导致显著的类型级计算开销。
**缓解方案**:主要选择是**消除或减少**此类构造。这可能涉及使用 Template Haskell,或将编译时间成本作为必要权衡接受。
2. **大型定义**:过大的类型定义或表达式会增加编译器负担。
## 结语
优化快速构建是一个持续性的研究过程。目标是建立项目编译特性的心理模型,在代码抽象(如使用泛型)与构建性能之间做出明智的权衡决策。此过程不仅能实现更快的构建,还能加深对系统的理解。
来源:使用 GHC 优化快速构建(https://www.youtube.com/watch?v=nmE6aa_I5TU)
相似文章
借用生物学家的方法来更快速地编译Haskell
本文探讨了GHC中最优的ApplicativeDo调度问题(该功能因性能缓慢默认关闭),并将其与RNA折叠中使用的动态规划算法进行类比,以改善编译器的性能。
编写快速编译器
这篇文章描述了编写快速编译器的各种技巧和策略,专注于最小化代码执行、减少内存使用和优化常见路径,以实现每秒超过50万行代码的编译速度。
优化 #[sqlx::test] 的重建时间
一位 Rust 开发者对 SQLx 测试的增量重建时间进行了性能分析和优化,识别了调试信息生成和过程宏开销等瓶颈,并提出了加速测试编译的改进方案。
为变更优化,而非应用性能
本文指出,软件团队常常过度优化微性能基准测试,却牺牲了开发者体验和工程吞吐量,而这两者才是长期交付速度与可维护性的真正瓶颈。
主机调优GCC以加快编译速度
这篇博客文章介绍了如何使用配置文件引导优化、LTO和-O3构建主机调优的GCC编译器,以实现更快的编译速度,并附有详细的说明和基准测试。