作为现代编译器基础设施的基石,LLVM 支撑着从 Rust、Swift 到 Clang 等众多语言的编译后端,其设计质量直接影响着整个软件开发生态系统的效率与可靠性。然而,随着项目规模膨胀至数百万行 C++ 代码,LLVM 在架构层面积累的设计缺陷逐渐显现,成为制约其进一步发展的瓶颈。本文基于 LLVM 项目负责人 Nikita Popov 的最新分析,系统梳理 LLVM 的核心设计问题,并提出切实可行的改进路径。
高层架构问题:规模与效率的失衡
审查容量不足:贡献者多而审查者少
LLVM 项目拥有数千名活跃贡献者,贡献分布相对均匀,这通常被视为开源项目的健康指标。然而,问题的核心在于审查容量严重不足。代码审查需要比编写代码更高的专业水平,且对审查者个人或其雇主往往不产生直接价值回报。这种不对称导致 PR 经常长时间等待合格审查,最终可能由非专业领域的同事 "橡皮图章" 式通过。
更糟糕的是,LLVM 采用了一种独特的贡献模型:PR 作者需要主动请求审查者。对于新贡献者而言,他们根本不知道应该找谁审查。虽然标签通知系统能在一定程度上缓解这个问题,但 UI 设计并未明确展示这一机制,导致许多 PR 被遗漏。一个潜在的改进方案是引入类似 Rust 的 PR 分配系统,自动为 PR 匹配合适的审查者。
API 不稳定:灵活性的代价
LLVM 的 C++ API 和中间表示(IR)都处于不断变化中,这既是优势也是劣势。优势在于 LLVM 能够持续演进,勇于纠正历史错误;劣势则在于这种不稳定性给用户带来了沉重的维护负担。前端项目可以通过相对稳定的 C API 获得一定程度的隔离,但大多数主要前端仍需要额外的绑定来使用不稳定的 C++ API。
对于那些与 LLVM 深度集成的用户(如下游后端),他们别无选择,只能跟上所有 API 变化。这种 "上游或离开" 的开发哲学意味着,如果你不将代码贡献到上游,那么上游的决策过程也不会考虑你的需求。正如 Nikita Popov 所指出的:"在 LLVM 中进行重大更改已经极其困难,因为项目规模庞大,如果再增加额外的稳定性约束,情况会更加复杂。"
编译时间瓶颈:优化优先的设计哲学
LLVM 项目本身就是一个庞然大物:核心代码超过 250 万行 C++,整个单仓库接近 900 万行。C++ 本就以编译缓慢著称,编译如此庞大的代码库需要大量时间。在低配置笔记本电脑上构建 LLVM 几乎是一场噩梦。
更令人担忧的是,LLVM 在-O0(无优化)级别的编译性能尤其糟糕。其架构是为深度优化而设计的,即使不进行任何优化,仍然需要承担大量基础设施开销。LLVM TPDE 替代后端的研究表明,通过重新设计,-O0编译时间有可能提升一个数量级。
IR 设计缺陷:历史包袱与理论挑战
undef 值的语义困境
undef值代表从特定集合中取任意值,传统上用于建模未初始化值和延迟的未定义行为。虽然 poison 值已经取代了后者,但undef仍然用于表示未初始化内存。这带来了两个主要问题:
首先是多用途问题:一个undef值在不同使用点可以取不同的值。这意味着增加使用计数的转换通常是无效的,基于值相等的优化必须格外小心。undef值的存在阻止了我们执行某些期望的优化,或者大大增加了它们的复杂性。
其次是推理困难:人类难以理解undef的语义,对于证明检查器来说,处理undef在计算上非常昂贵。未来很可能用 poison 值替代内存中的undef,但这需要 LLVM 能够正确处理内存中的 poison 值,可能需要引入字节类型等新的 IR 特性。
约束编码的碎片化
优化编译器的一个关键挑战是如何编码约束条件(如 "此值为非负" 或 "此加法不会溢出")。这包括前端提供的约束(基于语言未定义行为规则)和编译器生成的约束。
LLVM 有多种编码额外约束的方式:poison 标志、元数据、属性、assume 指令等。这些方法在信息编码能力、优化过程中保留的可靠性以及对优化的负面影响方面都存在权衡。元数据中的信息丢失得太频繁,而 assume 指令中的信息又丢失得不够频繁。
浮点语义的复杂性
一旦离开 "严格符合 IEEE 754 默认环境中的浮点数" 这个理想世界,浮点语义就会出现各种问题。信号 NaN 和浮点异常的处理、非默认浮点环境的建模都是挑战。LLVM 使用受限浮点内在函数来表示这些情况,但这并不理想,因为所有浮点处理都被分割成两个平行的宇宙。
此外,非规格化数的处理也是一个问题。LLVM 有一个函数属性来不假设 IEEE 非规格化行为,但这只适用于全局使用刷新到零(FTZ)的情况。对于像 ARM 这样的架构,标量操作符合 IEEE 标准,而向量操作使用 FTZ,这个属性就没有帮助。
工程实现问题:部分迁移与技术债务
漫长的部分迁移过程
由于 LLVM 规模庞大,任何重大更改都既困难又耗时。迁移过程往往持续数年,期间两种不同的实现共存,直到所有代码都完成迁移。两个典型的例子是:
新 Pass 管理器:这个 "新"Pass 管理器首次引入已超过十年。大约五年前,我们开始默认将其用于中端优化流水线,并放弃了对传统 Pass 管理器的支持。然而,后端仍然使用传统 Pass 管理器。虽然支持代码生成中新 Pass 管理器的工作正在进行,但要所有目标都完成移植并完全淘汰传统 Pass 管理器,还需要相当长的时间。
GlobalISel:这是一个更极端的案例。GlobalISel 是旨在替代 SelectionDAG(和 FastISel)的 "新" 指令选择器。它大约在十年前引入,但至今没有一个原本使用 SelectionDAG 的目标完全迁移到 GlobalISel。有一个新目标是 GlobalISel 专用的,还有一个在未优化构建中默认使用 GlobalISel。但除此之外,SelectionDAG 仍然是所有地方的默认选择。
ABI / 调用约定处理的混乱
LLVM 中调用约定的处理几乎是一团糟。处理调用约定的责任在前端和后端之间分割。这本身不是问题 —— 问题在于完全没有文档说明前端和 LLVM 之间的调用约定契约。实现 C FFI 的正确方法基本上是查看 Clang 做了什么并复制它(不可避免地会有错误,因为规则可能非常微妙)。
Rust 在目标特性与调用约定的交互方面遇到了很多困难。启用额外的目标特性可能会改变调用 ABI,因为额外的浮点 / 向量寄存器开始用于参数 / 返回传递。这意味着在启用特性的函数和禁用特性的函数之间的调用可能不兼容,因为它们假设不同的 ABI。
理想情况下,ABI 和目标特性应该是正交的,只在某些 ABI 需要特定目标特性时耦合(例如,没有启用 FP 寄存器就不能有硬浮点 ABI)。目标特性是每个函数的选择,而 ABI 应该是每个模块的选择。
改进路径与替代架构思路
渐进式重构策略
面对 LLVM 的规模和技术债务,激进的重写是不现实的。更可行的策略是渐进式重构:
-
建立更好的性能跟踪基础设施:LLVM 目前缺乏官方的、易于访问的公共性能跟踪基础设施。虽然有一个 LNT 实例,但它目前处于损坏状态,用户体验极差,提交的数据很少,并且无法为 PR 请求测试运行。建立第一方性能跟踪将帮助贡献者评估其更改的运行时性能影响。
-
加强端到端测试:LLVM 的单元测试覆盖率很好,但整个优化流水线的测试覆盖不足,中端和后端流水线组合的测试几乎不存在。需要建立更全面的端到端可执行测试,覆盖不同配置和类型的基本操作。
-
统一后端实现:减少后端之间的分歧,推动通用优化而非目标特定的解决方案。虽然目标特定的优化有时是必要的,但过度使用目标钩子会增加维护负担和代码重复。
替代编译器基础设施的兴起
LLVM 的设计缺陷催生了一批替代编译器基础设施的兴起,它们在不同方面提供了改进思路:
Cranelift:作为 Rust 项目的替代代码生成后端,Cranelift 专注于快速编译。在大型项目(如 Zed、Tauri、hickory-dns)的测量中,Cranelift 目前能将代码生成时间减少约 20%,相当于干净构建的总编译时间加快约 5%。Cranelift 的设计哲学更注重编译速度而非深度优化,这使其特别适合开发周期。
MLIR:由 LLVM 创始人 Chris Lattner 创建,MLIR 旨在解决 AI 软件系统的碎片化问题。它提供了一个模块化、可扩展的编译器基础设施,可以跨硬件平台、软件框架和快速演进的机器学习需求进行扩展。MLIR 的设计吸取了 LLVM 的经验教训,特别是在中间表示的可扩展性和模块化方面。
具体可落地参数与监控要点
对于希望改进 LLVM 集成或基于 LLVM 构建系统的团队,以下参数和监控点值得关注:
-
编译时间基准:建立
-O0、-O1、-O2、-O3不同优化级别的编译时间基准线,监控回归情况。特别关注-O0性能,因为这是开发体验的关键。 -
内存使用模式:监控 LLVM 在不同编译阶段的内存使用情况,特别是处理大型代码库时的峰值内存消耗。
-
API 稳定性指标:跟踪 C++ API 和 IR 的变化频率,评估维护成本。对于深度集成的项目,考虑抽象层设计以减少直接依赖。
-
后端一致性检查:建立跨后端的功能和性能测试,确保优化和修复在所有目标架构上一致应用。
-
审查效率指标:监控 PR 从提交到合并的平均时间,识别审查瓶颈,优化审查流程。
结语:在演进中寻找平衡
LLVM 的设计缺陷并非偶然,而是大规模复杂系统演进的必然结果。其成功之处在于提供了一个相对统一、功能强大的编译器基础设施,支撑了现代编程语言的繁荣发展;其不足之处则反映了在性能、正确性、可维护性和演进能力之间寻找平衡的永恒挑战。
正如 Nikita Popov 所强调的,指出这些问题 "不是不使用 LLVM 的理由,而是改进 LLVM 的机会"。对于编译器开发者和系统架构师而言,理解这些设计缺陷不仅有助于更好地使用 LLVM,也为构建下一代编译器基础设施提供了宝贵的经验教训。
在可预见的未来,LLVM 仍将是编译器生态系统的核心组件。通过渐进式改进、更好的工程实践和对替代方案的开放态度,我们可以在保持现有生态系统稳定的同时,逐步解决这些深层次的设计问题。这需要整个社区的共同努力,包括更好的测试基础设施、更严格的性能跟踪、更统一的开发流程,以及对新技术架构的持续探索。
资料来源
- Nikita Popov, "LLVM: The bad parts", 2026 年 1 月 11 日
- LLVM 社区讨论,"If LLVM is so slow is anything being done about it?", LLVM Discourse 论坛
- Rust 项目目标,"Production-ready cranelift backend", 2025 年
- Modular 博客,"What about the MLIR compiler infrastructure?", 2025 年 4 月 8 日