在编译器工程领域,Nikita Popov(npopov)是一个独特的存在。他既是 LLVM 项目的 lead maintainer,负责这个拥有超过 250 万行 C++ 代码的巨型编译器基础设施,又是 PHP opcache 优化器和 JIT 编译器的主要开发者。这种双重身份赋予了他审视编译器架构的独特视角 —— 既理解大型通用编译器系统的复杂性,又深知轻量级专用编译器的实际需求。2026 年 1 月,npopov 发表了一篇题为《LLVM:糟糕的部分》的技术文章,从内部维护者的角度系统性地剖析了 LLVM 的架构缺陷,这为我们理解现代编译器设计哲学提供了宝贵的窗口。
LLVM 架构缺陷的系统性批判
npopov 在文章中列举了 LLVM 的 12 个主要问题,这些缺陷不仅反映了技术实现上的挑战,更揭示了通用编译器基础设施在设计哲学上的深层矛盾。
1. 评审能力不足与代码质量风险
LLVM 拥有数千名贡献者,但评审能力严重不足。npopov 指出:“有太多人写代码,但评审的人太少。” 这种不平衡导致 PR 经常长时间得不到合格评审,最终可能由非专业领域的同事 “橡皮图章” 式通过。更糟糕的是,LLVM 采用了一种特殊的贡献模型 ——PR 作者需要自己请求评审者,这对于新贡献者尤其困难,他们往往不知道应该找谁评审。
2. API 与 IR 的频繁变更代价
LLVM C++ API 和 IR 都不稳定,经常发生变化。这既是 LLVM 的优势(能够纠正过去的错误),也是其弱点 —— 变更给用户带来了沉重负担。前端可以通过相对稳定的 C API 获得一定程度的隔离,但紧密集成 LLVM 的用户(如下游后端)必须跟上所有 API 变化。npopov 尖锐地指出,LLVM 的开发哲学可以概括为 “上游或滚蛋”(upstream or GTFO)—— 如果你不上游贡献代码,那么你的需求也不会被纳入上游决策考虑。
3. 构建与编译时间的双重挑战
LLVM 本身超过 250 万行 C++ 代码,整个 monorepo 约 900 万行。在低配置笔记本电脑上构建 LLVM 是痛苦的体验。更关键的是编译时间问题:LLVM 很慢,这对于 JIT 用例和生成大量 IR 的语言(如 Rust 或 C++)都是问题。npopov 特别提到,LLVM 在 - O0 编译时间上表现尤其糟糕,因为其架构是为优化而设计的,即使不进行优化,许多成本仍然存在。
4. 测试基础设施的全面缺失
LLVM 虽然有详尽的单元测试覆盖,但端到端可执行测试严重不足。llvm-test-suite 仓库中的测试数量有限,且不全面覆盖基本操作。npopov 批评道:“部分原因是 C/C++ 测试的限制,但这不能成为将测试委托给 Zig 的借口。” 后端分化问题也因缺乏端到端测试而加剧 —— 开发者往往只为自己关心的后端修复问题,导致不同后端实现越来越不一致。
5. IR 设计的根本性问题
在 IR 设计层面,npopov 指出了几个核心问题。undef 值允许在不同使用点取不同值,这增加了优化的复杂性。规范不完整问题长期存在,许多已知的 miscompilation 问题因涉及 IR 设计变更而难以修复。约束编码问题也困扰着 LLVM—— 有太多不同的方式来编码额外约束(poison 标志、元数据、属性、assume),每种都有在优化过程中信息保留可靠性的权衡。
PHP JIT IR 框架的设计哲学对比
与 LLVM 的通用性追求形成鲜明对比的是,PHP JIT 采用了完全不同的设计哲学。PHP 的下一代 JIT 基于独立的 IR 框架,其核心设计体现了专用编译器的务实选择。
Sea-of-Nodes 单一 IR 贯穿始终
PHP JIT IR 框架在整个编译流水线中使用单一的 Sea-of-Nodes 中间表示。这与 LLVM 的多阶段 IR 转换(从 LLVM IR 到 SelectionDAG 再到 MachineInstr)形成直接对比。单一 IR 简化了编译器内部结构,减少了表示转换的开销和复杂性。正如 PHP JIT IR 框架 RFC 所述:“框架提供简单的 IR 构造 API,在 IR 构造期间工作的折叠引擎,稀疏条件常量传播,全局代码移动,简单的指令选择器,线性扫描寄存器分配器和内存代码生成器。”
编译速度优先的设计决策
PHP JIT 明确将编译速度作为首要设计目标。在测试中,带有新 JIT 的 PHP 每秒可生成约 15MB 本地代码。这种速度优先的设计反映了 JIT 编译器的实际需求 —— 在运行时编译代码,编译延迟直接影响用户体验。相比之下,LLVM 的架构更注重生成高度优化的代码,即使这意味着更长的编译时间。
轻量级与自包含的架构
PHP JIT IR 框架被设计为轻量级且自包含的。必要的 IR 框架部分被嵌入到 PHP 源代码树中,不引入新的外部依赖。这种设计减少了集成复杂性,也降低了维护负担。对于像 PHP 这样的脚本语言运行时,编译器的复杂度和内存占用都是关键考量因素。
编译器设计范式的工程实现路径
从 npopov 的双重经验中,我们可以提炼出替代性编译器基础设施的几个关键工程实现路径。
路径一:分层架构与明确边界
成功的编译器基础设施需要在通用性和专用性之间找到平衡。一种可行的路径是采用分层架构:底层提供稳定的基础 IR 和优化原语,上层针对特定用例(如 JIT、AOT、特定语言)构建专用抽象。这种分层允许在不同层次上进行不同的稳定性承诺 —— 底层接口可以相对稳定,而上层可以根据需要更灵活地演进。
路径二:编译时与运行时的明确分离
LLVM 的一个核心问题是其架构同时服务于 AOT 编译和 JIT 编译,导致设计妥协。替代方案是明确分离编译时和运行时组件。编译时组件可以更复杂、进行更深度的优化;运行时组件则需要轻量级、快速启动。这种分离允许为不同场景优化不同的组件,而不是试图用一个架构满足所有需求。
路径三:增量优化与反馈导向
传统的编译器架构假设一次性完整优化。但对于 JIT 和动态语言,增量优化和基于反馈的优化可能更有效。编译器基础设施应该支持逐步优化 —— 先快速生成可运行代码,然后基于实际执行反馈进行渐进式优化。这种架构需要不同的 IR 设计,能够支持部分优化和增量更新。
路径四:模块化与可组合性
LLVM 的庞大代码库使得修改变得极其困难。替代方案是采用更模块化的设计,将编译器分解为独立的、可组合的组件。每个组件有清晰的接口和明确的职责,可以独立开发、测试和替换。这种架构降低了参与门槛,也使得实验新想法更加容易。
可落地的技术参数与监控要点
基于 npopov 的分析,我们可以为编译器基础设施项目制定具体的工程实践清单:
代码质量与评审参数
- 评审响应时间阈值:设置 PR 评审响应时间目标(如 48 小时内获得初步反馈),并监控达标率
- 评审专家分布:确保每个核心模块至少有 3-5 名合格的评审专家,避免单点故障
- 自动化评审辅助:实现类似 Rust 的 PR 分配系统,自动为新 PR 分配合适的评审者
性能监控指标
- 编译时间跟踪:建立编译时间跟踪系统,监控关键工作负载的编译时间变化
- 内存使用分析:监控编译器本身的内存使用情况,特别是对于 JIT 用例
- 代码生成质量:建立公开的性能跟踪基础设施,使贡献者能够评估变更对性能的影响
测试覆盖要求
- 端到端测试比例:确保至少 30% 的测试是端到端可执行测试
- 跨后端测试:为所有支持的后端运行相同的测试套件,确保行为一致性
- 回归测试自动化:实现自动化的回归检测,当性能或正确性回归时立即通知
API 稳定性策略
- 稳定性承诺分级:对不同接口提供不同级别的稳定性承诺(稳定、实验性、内部)
- 变更影响评估:对任何 API/IR 变更进行全面的下游影响评估
- 迁移工具支持:为重大变更提供自动化迁移工具,降低用户升级成本
结论:在通用性与专用性之间寻找平衡
npopov 的批判视角揭示了编译器工程中的一个核心困境:如何在提供强大功能的通用性和满足特定需求的专用性之间找到平衡。LLVM 代表了通用性的一端 —— 它试图为所有可能的编译器用例提供支持,但这种雄心带来了复杂性、维护负担和性能妥协。PHP JIT IR 框架则代表了专用性的一端 —— 它针对特定场景(PHP 运行时 JIT 编译)进行优化,获得了简单性和速度,但牺牲了通用性。
真正的创新可能在于找到第三条道路:构建既足够通用以支持广泛用例,又足够专用以在关键场景中表现出色的编译器基础设施。这需要重新思考编译器架构的基本假设 —— 放弃 “一刀切” 的幻想,接受不同用例需要不同优化的现实。
npopov 作为同时参与两个极端项目的开发者,他的经验尤为宝贵。他既理解 LLVM 的复杂性来自何处,也了解简化设计的价值。对于编译器工程师来说,关键不是选择 LLVM 或 PHP JIT 的道路,而是理解每种设计背后的权衡,并根据具体需求做出明智的选择。
在编译器基础设施的未来发展中,我们可能会看到更多混合架构的出现 —— 既有通用组件提供基础能力,又有专用组件针对特定场景进行优化。这种分层、模块化的方法可能最终解决 npopov 所揭示的 LLVM 困境,为下一代编译器基础设施开辟新的可能性。
资料来源:
- Nikita Popov, "LLVM: The bad parts", npopov.com, January 11, 2026
- PHP RFC: A new JIT implementation based on IR Framework, wiki.php.net, September 20, 2023