LLVM 23 编译时间改进
摘要
LLVM 23 在 -O3 构建中实现了编译时间减少 6.75%,这主要得益于 ADT 哈希映射、集合和其他组件的改进。
<p><a href="https://lobste.rs/s/ku4v3b/compile_time_improvements_llvm_23">评论</a></p>
查看缓存全文
缓存时间: 2026/08/22 07:05
# LLVM 23 中的编译时间改进
来源:https://aengelke.net/llvm23-ct.html
*LLVM 23 在 -O3 构建中实现了显著的编译时间改进,缩减了 6.75%(sqlite3 更是减少了 10.53%)。本文将介绍这些改进的主要来源。*
除非另有说明,所有性能数据均指 LLVM 编译时间跟踪器上的 stage2-O3 配置。
## ADT
LLVM 广泛使用的哈希表/集合经历了三项重大改进(此处也有描述 https://maskray.me/blog/2026-06-07-recent-llvm-hash-table-improvements):首先,从二次探测哈希表转向线性探测并改进了删除操作(DenseMap 减少 -1.27% (https://llvm-compile-time-tracker.com/compare.php?from=90779840d51aa3fff6fa030ade7150bc647ac5ff&to=1f10f1ca8af3dff956b353d7ff3ea169c82ed909&stat=instructions:u),SmallPtrSet 减少 -0.24% (https://llvm-compile-time-tracker.com/compare.php?from=5d5220c53848ef86de57946acda4bc7d8b91880c&to=7b227a281d29861c959db0d08c02fc97b55709e0&stat=instructions:u),StringMap 减少 -0.10% (https://llvm-compile-time-tracker.com/compare.php?from=6f3fd2accfd3e1cdbf537007415a5ec47ddd12a8&to=a296bdef76b9e9c6effbb6d052e75b3ef3670f3e&stat=instructions:u)),消除了对墓碑键的需求。其次,DenseMap 的占用率现在存储在一个紧凑的位数组中(+0.13% (https://llvm-compile-time-tracker.com/compare.php?from=d7a23b7ab0afad10d26f0f1a5edfce88de452f81&to=1e3dc606df3aefc924fb58238dd705b8bfde1581&stat=instructions:u)),而不是使用空键,这避免了需要任何带内保留值。虽然这在 Clang 构建的 Clang 的指令数方面表现更差,但它改善了时钟周期并减少了分支和缓存未命中。作为副作用,移除空键和墓碑键也使得哈希表查找更高效(-0.04% (https://llvm-compile-time-tracker.com/compare.php?from=c384de1c688bab5d5d1fbf1bb3d31895c98e34cd&to=3232b4d0e07802a1852874910815489dc3e4774a&stat=instructions:u)),因为一些相等函数不再需要显式检查这些键。第三,从 CityHash 和弱指针哈希函数转向 xxh3(-0.18% (https://llvm-compile-time-tracker.com/compare.php?from=525fab579da1504be3f09013f22e9ad499fb5bfc&to=71d78b2220e4dc4b022fd74aec16ed8d93fc419e&stat=instructions:u))已经改善了旧哈希表的性能,并且是前述更改的前提条件。
在 SmallVector 中,可平凡复制的 `push_back` 增长路径被移出函数体并改为允许尾调用优化(此处也有描述 https://maskray.me/blog/2026-06-27-a-deep-dive-into-smallvector-push-back)(-0.50% (https://llvm-compile-time-tracker.com/compare.php?from=c6b0a8d2f99aa81978ec12170e27ade8f138aebd&to=fe1fc78c3e49eea31e4ba7647727680665923dd7&stat=instructions:u)),在某些情况下导致寄存器的活跃范围更短,快速路径上的指令更少,更多的 shrink wrapping,以及更小的代码尺寸从而带来更多的内联。
`BumpAllocator` 进行了一些清理(-0.17% (https://llvm-compile-time-tracker.com/compare.php?from=fc43f7d5d120c5eb5dda4baef42db41491f178dd&to=ca59c69132eef55cc42bf8854706590dfddf5584&stat=instructions:u),+0.06% (https://llvm-compile-time-tracker.com/compare.php?from=98dc5c68a7672138e60e9869405aded6db10b31e&to=e646fa6c1cebdddc4bdb81703c95ad5ef61c286d&stat=instructions:u))。由于内联启发式,编译时间数据有些混合;更小的分配函数改变了内联边界,导致了不同的“奇偶”内联模式。(例如,对于 A -> B -> C -> D,如果 D 未被内联,B 将被内联到 C;如果 D 变小了,它将被内联到 C,但 C 将不再被内联到 B,而 B 将被内联到 A——但这可能会错过将 C 内联到 B 时可能的重要简化机会。)
`post_order` 遍历被重写(-0.18% (https://llvm-compile-time-tracker.com/compare.php?from=7a186da65b3754509bff2d7bafcc28a095279320&to=691a130e0f14459d9358a71ffd52a01295e6200a&stat=instructions:u)),不再将遍历状态存储在迭代器本身中,虽然仍不理想,但这使得迭代器移动更便宜,并在一些迭代器函数中启用了内联。
## 支配树
支配树表示从存储子节点向量改为子节点-兄弟节点表示法(-0.13% (https://llvm-compile-time-tracker.com/compare.php?from=ae425ab913afc3f7a8326dfa0b06bc46746b2cc7&to=284ef1b4acff8d1d9b962a5cd0497fc9255b52cf&stat=instructions:u)),避免了分配。需要注意不要改变子节点的顺序,因为几个 pass 依赖于此,并且如果顺序颠倒,会产生实质不同的输出。使用 bump allocator(-0.50% (https://llvm-compile-time-tracker.com/compare.php?from=f4de9b89e9489deda24b6143c574ee8688d4a734&to=7b6751916cf74692df9c331ecdc731c42addafdf&stat=instructions:u))用于节点,显著减少了对 malloc()/free() 的调用次数,考虑到编译期间构建的支配树的数量。
支配树构建经历了一些改进,其中最显著的是不实例化后继节点(-0.21% (https://llvm-compile-time-tracker.com/compare.php?from=4a2ad970c6fe783f026be47292be93b6cdfe7d9b&to=e395479d5cf45246f8bd6636a72274df8f225be5&stat=instructions:u))以及将前驱节点存储为边列表(-0.11% (https://llvm-compile-time-tracker.com/compare.php?from=5ec0eee3a67ec0f8aebccc6864c574ce73e2f937&to=55669ac2f7df5e54561a3c36df6322a6318cb575&stat=instructions:u)),提供了最大的单项改进。
尽管构建算法即使在较大的程序上也相当快(尽管最坏情况下是 O(n²)),但支配树表示仍然相当低效,这主要是为了保持与现有遍历模式的兼容性并支持更新。实际上,相当一部分构建时间纯粹用于将结果实例化到 `DominatorTreeBase` 数据结构中。
## IR 数据结构
将 `successors()` 实现为对 `Use` 范围的迭代器(-0.21% (https://llvm-compile-time-tracker.com/compare.php?from=b85e7f04397752926175d72b953fa428f08c656a&to=5cc4594154d96730ecbf34968779a28b1ab757ab&stat=instructions:u))解决了一个长期存在的低效问题:以前,每个 use 访问都是一个函数外调用,反复分派终止指令类型。这样做需要一些准备工作以确保后继节点在所有终止指令中连续存储(`SwitchInst` 需要更改,case 值不再是 `Use`,而是普通的 `ConstantInt*`),以及更大的工作量将 `Br` 操作码拆分为独立的 `UncondBr` 和 `CondBr` 操作码 (https://discourse.llvm.org/t/rfc-split-branchinst-into-uncondbr-and-condbr/90022)(-0.08% (https://llvm-compile-time-tracker.com/compare.php?from=c7aaaeaef4ec39ee17d0400ce6250e22e5bd85e5&to=4fd826d1f9dc40af309d4500f671eec22be8baaf&stat=instructions:u))以避免使用位域访问来区分它们。尽管如此,`successors()` 仍然位居最热函数(自身时间)前 15 名,主要是由于访问终止操作码时的缓存未命中和对终止类型进行 switch 时的分支未命中。
要求 `BasicBlock::getTerminator()`(-0.07% (https://llvm-compile-time-tracker.com/compare.php?from=c32d670757b81aefd3acf3c228667a82d9237e97&to=75814307221558d6f3dc4555e75e57c94c6ec85a&stat=instructions:u))和 `successors()`(-0.12% (https://llvm-compile-time-tracker.com/compare.php?from=de81419c31e83f059d9acee70e06be01d5dfd862&to=36041192cf6c9042615c8d2364cacc698ef89865&stat=instructions:u))具有良构的 IR,以及要求支配树中的块非空(-0.06% (https://llvm-compile-time-tracker.com/compare.php?from=0f1ec17f29f145d3c0f1439316ab9203f05d456a&to=43ec60eee5f95e01c3b92ed08d09d574556c62c8&stat=instructions:u))也带来了改进——即使廉价的检查,如果执行得足够频繁,也会有些昂贵。同样,前驱节点迭代变得更快:LLVM 通过使用列表存储基本块的前驱节点,终止指令使用后继块。以前,基本块的另一种用户是 `BlockAddress`,它非常少见(仅在 C 语言的 computed goto 中需要),因此前驱迭代器必须检查每个块使用是否是终止指令。将 BlockAddress 改为不再使用基本块(-0.06% (https://llvm-compile-time-tracker.com/compare.php?from=143338131f0e8f1586f7f9aacb7d94dfde169b08&to=080286d051efe040c8678aef122d235785d74d5e&stat=instructions:u))使得可以移除此检查。移除对如今非规范化的、基于 `icmp`+`select` 的整数最小/最大值的模式匹配(在几个版本前已被规范化为专用内部函数)带来了一些改进(-0.09% (https://llvm-compile-time-tracker.com/compare.php?from=383f8586986aac5abec446a08b1c33a0fff0a631&to=ff066c745de9f00b0bac8a98d1bd9bd864034da0&stat=instructions:u)),主要是由于更小的、现在被内联的模式匹配函数。
相当多的指令附加有元数据,例如用于调试信息或基于类型的别名分析。调试信息有针对指令的快速路径,但所有其他元数据附件都存储在上下文中,以前存储在以 `Value` 指针为键、映射到附件向量的哈希表中。将这些附件存储在单个向量中(-0.35% (https://llvm-compile-time-tracker.com/compare.php?from=ccfba7736f2d39434ae768ae9c9c0165b1959c4b&to=282fb05292cab555010a645cef88d62c846511d4&stat=instructions:u))(在向量条目上形成多个链表)并将附件列表的起始位置存储在指令中,使得元数据查询便宜得多。使用 `SmallVector` 的缺点是增长时所有 `TrackingMDNodeRef` 都需要移动,但对增加间接层的数据结构(例如修改过的 `PagedVector`)的实验产生了更差的性能。然而,元数据仍然是一种昂贵的机制,很可能具有元数据(特别是与别名分析相关的)的指令应该在某个时候将此信息内联存储。元数据仍然昂贵可以通过使 InstCombine 的 `!annotation` 元数据访问变为惰性的更改来证明(-0.07% (https://llvm-compile-time-tracker.com/compare.php?from=833d2418ffe9647b8a88f774208ec20c68805f73&to=3fc92aeb55d5852dfb32ec0061e2b41c2424856c&stat=instructions:u))。调试信息元数据得到改进,不再使用 TrackingMDNodeRef 而是普通的 MDNode 指针(stage2-O3: -0.50%, stage2-O0-g: -1.15% (https://llvm-compile-time-tracker.com/compare.php?from=fa02a6ed66b1700c996b49c96c6bc0eb014c9518&to=91b77dc685cf628cbf925e43e25f2f86a912b38b&stat=instructions:u))来引用调试位置;这是可能的,因为调试位置从不被替换。`IRBuilder` 失去了附加任意元数据的能力(-0.04% (https://llvm-compile-time-tracker.com/compare.php?from=115c749cbdada7c2663d4a64c166bafccab5fcfa&to=9550cd76cadea9f25a5c0bf3ee06e9f7ce5631ed&stat=instructions:u)),节省了对每个插入指令几乎从未命中的检查。
`Constant::isNullValue` 被改为急切计算并将该位存储在 SubclassOptionalData 中(-0.14% (https://llvm-compile-time-tracker.com/compare.php?from=80f6b7641ef95d435a9e641970291375e3013cbe&to=da3f152eebca11285f33777bf3bb97f69fb9f10d&stat=instructions:u)),避免了频繁对常量类型进行 switch。
## 块编号
移除了几个以基本块为键的哈希表,继续我于 2024 年通过引入块编号开始的工作。最初,为了与 Machine IR 共享基础设施,所有使用这些数据的数据分析都必须支持块被重新编号的情况。事实证明这具有限制性,并阻止了在例如 LoopInfo 中的采用,在那里重新编号不容易支持。在 Machine IR (https://github.com/llvm/llvm-project/commit/9a2f23e1a40b31ac1b1ed7c305ba285efe99b903) 中引入单独的、更稳定的分析块编号,解锁了其他几种用途。现在 MemoryDependenceAnalysis(-0.14% (https://llvm-compile-time-tracker.com/compare.php?from=efdbf7b815b7c8fa161819e87050154d62b3d146&to=4a7213867abe6a61e3c0baed27fcfc1015270cfa&stat=instructions:u))、BlockFrequencyInfo(-0.15% (https://llvm-compile-time-tracker.com/compare.php?from=92b595b9b4ca71aad9e7cc7d32e4b90a9b051a5b&to=89665812f54feeab985c0c8b72cf3063128e65b2&stat=instructions:u))、LazyValueInfo(-0.02% (https://llvm-compile-time-tracker.com/compare.php?from=065a39b9f7f06fca0926394096ee1c1fac41d446&to=89431a368e022d50c42d7eb38327cafe0ae880a0&stat=instructions:u))、LoopInfo(-0.15% (https://llvm-compile-time-tracker.com/compare.php?from=333ac33be6f4627a8f697b8f8a617c6d5da632a6&to=0d05c882ce9976de790e690b92e2673088591e36&stat=instructions:u))、BranchProbabilityInfo(-0.05% (https://llvm-compile-time-tracker.com/compare.php?from=5863fcbbe5b23b75db46af6bcd9fd65c0149a9c7&to=56c579e288f374d4a8bd296c7aa97fae40dffedc&stat=instructions:u))、`removeUnreachableBlocks()`(-0.03% (https://llvm-compile-time-tracker.com/compare.php?from=57427f84fe5fdda71aef4be257ed28d7b4f55d05&to=926bea91c14893ba661393e44882fae83b446827&stat=instructions:u))和后序遍历(-0.18% (https://llvm-compile-time-tracker.com/compare.php?from=3dda4b54634f1fef31d372c7a3a41438770e7c04&to=3b5910de4223f0efafabeb352956d6711fd84c59&stat=instructions:u))也使用了块编号。在 SimplifyCFG 之后重新编号块(-0.05% (https://llvm-compile-time-tracker.com/compare.php?from=926bea91c14893ba661393e44882fae83b446827&to=dab1e30d39fec6c17e0e5a8dd1d2a3dbb2a9aa96&stat=instructions:u))也有助于保持编号密集;在更多地方这样做可能是有益的。
使用块编号代替块指针的一个副作用是,BlockFrequencyInfo 和 BranchProbabilityInfo 不再需要使用昂贵的 ValueHandles 从其数据结构中移除基本块(这对于防止指针被重用时的数据错误是必要的)。ValueHandles 仍然是开销的一个重要来源,从长远来看可能应该被移除。
## 后端
GlobalISel,这个可能最终取代的选择指令后端,已经进行了相当多的改进,主要集中在 AArch64 -O0,它是默认的。与 FastISel 相比的端到端减速从 12.71% 下降到 9.39%(sqlite3:从 31.81% 下降到 26.13%)。我最喜欢的改进是从 -O0 管道中删除了本地化器(stage1-aarch64-O0-g:-1.10% (https://llvm-compile-time-tracker.com/compare.php?from=d64dd5a2afea7c99a6c54827596e5f2172fe3342&to=e407fc3f3bca459977fca3f4e965d6df36524a7e&stat=instructions:u)),它以前导致编译时间在每个基本块的常量数量上呈二次增长。(可惜这被回滚了——这意味着在构建 Disarm (https://github.com/aengelke/disarm) 及其用户如 TPDE 时,我仍然必须强制禁用 GlobalISel。)GlobalISel 能在多大程度上赶上 FastISel 还有待观察。
除此之外,后端只有少数改进(-0.21% (https://llvm-compile-time-tracker.com/compare.php?from=1e87cdf03fb9d6659f7d039ebbb9a4c011947a49&to=48f50e89b726578ee87cd1305330fa32932bb685&stat=instructions:u),-0.10% (https://llvm-compile-time-tracker.com/compare.php?from=01dbfa0b6e04c09e389bafd7668b2f99bba3ea79&to=6d1eed609e08df72209e746bb39abbc494de8750&stat=instructions:u),-0.23% (https://llvm-compile-time-tracker.com/compare.php?from=eae0b6b2498305ee29dc85a405ede9ccdc10ce7d&to=08c052a2db407d2a21d468001fd2035d3720acf7&stat=instructions:u))。
## Clang
在 SimplifyCFG 中,将为 switch 生成静态查找表的最小密度从 40% 降低到 10%,改善了 C++ 程序的 Clang 性能(stage2-O0-g:-0.35% (https://llvm-compile-time-tracker.com/compare.php?from=15b1f3581f19f06d29d82b631217323a29d9247e&to=935565250c89185b68298256825860f88b42d6c0&stat=instructions:u))。在简化 switch 语句之前,SimplifyCFG 将函数划分为多个区域(块)。改进此划分(-0.10% (https://llvm-compile-time-tracker.com/compare.php?from=af11204d2f8e646493f80f11c3515b0e8425d518&to=860303440578c92c0247a775b0447a97853374b5&stat=instructions:u))并改进了在开关的非默认分支中直接使用默认情况(-0.07% (https://llvm-compile-time-tracker.com/compare.php?from=f840779a993239946a5252048b978574850b2c59&to=1d9c73446a56d3e94191507a71f929f84d0815c8&stat=instructions:u))。
Clang 前端对预处理和头文件使用了 48 位窗口哈希表。用 64 位索引将 32 位值扩展为 48 位,以允许更大的文件/更多的哈希桶,从而减少了碰撞(stage2-O3:-0.10% (https://llvm-compile-time-tracker.com/compare.php?from=57e0f6c8d7c639f8a5b12af69c37c428261d206c&to=8c5d057d0e57b39a4e8d693c15f88613f659359c&stat=instructions:u))。
对于 Clang C++ 模块,移除不必要的 `GlobalModuleIndex::lookup()` 调用(-0.05% (https://llvm-compile-time-tracker.com/compare.php?from=87245e465d1f6a17f3333333060a300c504743c4&to=d0c423825e579f25546764d5c19249120342532e&stat=instructions:u))和优化 `FileManager::getDirectoryEntry()`(-0.05% (https://llvm-compile-time-tracker.com/compare.php?from=06ed0d187596da3b0ad4495e9b4be702940e309e&to=c4af087c4c0233c6a002d3198e15686b543b692c&stat=instructions:u))带来了改进。
## 工具链
LLD 是 LLVM 的链接器。在 LLD ELF 中,为可重定位对象中的每个节分配唯一的 ID,以便直接查找(-0.07% (https://llvm-compile-time-tracker.com/compare.php?from=980e8d1c301a82ec8c34089b1a60ec3718d8243c&to=044f05272d88760c9c9f769d250d9f1d1f6544d1&stat=instructions:u)),并优化了重复符号的查找(-0.06% (https://llvm-compile-time-tracker.com/compare.php?from=f493b0d2f188e7c15679454b072022b6c93f32f3&to=343c9e760d048b095a2b8e204e7695a687368811&stat=instructions:u))。
编译器-运行时库 compiler-rt 的 BFloat16 实现从 C++ 改为 C(stage2-O3:-0.04% (https://llvm-compile-time-tracker.com/compare.php?from=89b59778d9443e5606391421e2d5e8c69f4c9d59&to=4f787315103b748e95615e8550986677164b552a&stat=instructions:u))。这使得编译器能够更积极地优化这些简单的数学函数,这些函数以前没有被优化,因为它们调用了其他函数。
Sanitizer 构建现在使用 `-ftrivial-auto-var-init=pattern`(-0.04% (https://llvm-compile-time-tracker.com/compare.php?from=7013484325f3b92654421c4420960946b0c70259&to=824a2a78b06d328e712115924b2e312d029548f5&stat=instructions:u)),这有助于变量初始化传递的常量传播。
## 审计
对 LLVM 源代码(不包括第三方代码)进行审计,以查找并删除未使用的头文件(stage2-O3:-0.46% (https://llvm-compile-time-tracker.com/compare.php?from=d8997778a21b01af67c41872bc68a898d913d6a5&to=c9993c10c1db073f69e5148d21231d819917863b&stat=instructions:u))。通过仅包含需要的头文件而不是传递依赖项,减少了包含文件的数量,从而提高了并行性。
`LLVM_ENABLE_DUMP` 在非调试构建中禁用,以在生产版本中删除所有 `dump()` 方法的定义(stage2-O3:-0.12% (https://llvm-compile-time-tracker.com/compare.php?from=48f7882862a45560110838007882863635127245&to=e01546041500085d1423385128c22866e71d662b&stat=instructions:u))。
对于 `LLVM_ENABLE_ABI_BREAKING_CHECKS`,一些检查被移除或条件化(stage2-O3:-0.23% (https://llvm-compile-time-tracker.com/compare.php?from=0c2d9e276e2352d913214c1028c032342094e0ad&to=2e87d7583c32364510918560340127827408f7b2&stat=instructions:u))。以前,`llvm_unreachable()` 的断言在每次执行时都被检查,现在它们仅在 `LLVM_ENABLE_REMARKS` 启用时被检查,对于发布版本通常是关闭的。`cast<>` 中的断言仅在启用断言时进行,但这是罕见的情况。因此,`assert` 宏现在需要显式包含,这有助于识别缺失的包含。
## 测试工具
LLVM 的单元测试框架 `gtest` 进行了现代化,例如使用 `std::variant`,并删除了 LLVM 自己的 `unittest/` 库(stage2-O3:-0.24% (https://llvm-compile-time-tracker.com/compare.php?from=19b0483022a987951e6573f2204b238
相似文章
优化 LLVM 的 bump 分配器
这篇博客文章详细介绍了对 LLVM 的 BumpPtrAllocator 进行的三项近期优化,通过移除冗余对齐、空指针检查以及每次分配的记账开销来减少快速路径开销,从而提升了 Clang、lld 及其他 LLVM 组件的性能。
Zig 构建速度正在提升
Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。
主机调优GCC以加快编译速度
这篇博客文章介绍了如何使用配置文件引导优化、LTO和-O3构建主机调优的GCC编译器,以实现更快的编译速度,并附有详细的说明和基准测试。
@bitCast 新语义与 LLVM 后端改进
Zig 语言引入了新的 @bitCast 语义,并通过更改整数降低(integer lowering)来避免编译错误,并更好地与编译器优化对齐,从而改进了其 LLVM 后端。
如何在2026年7月加速Rust编译器
Nicholas Nethercote报道了Rust编译器近期性能改进,包括平均墙钟时间总体减少5.59%,rustdoc大幅加速总计28%,以及通过PR和PGO训练更改实现的显著Clippy优化。