GC 与 safepoint 机制与 LLVM IR、优化 Pass 与 eBPF 后端

共 19 题
📑 题目列表 19 题
#
★★★

1. 分代 GC 把堆分为 Young/Old 代依据的是哪条弱分代假说?Minor GC 与 Full GC 的触发条件有何不同?

分代 GC 把堆分为 Young/Old 代依据的是哪条弱分代假说?Minor GC 与 Full GC 的触发条件有何不同?

  • 弱分代假说:大多数对象存活时间很短
  • 分代划分与新生代收集
  • Minor GC 与 Full GC 触发条件

分代 GC 依据弱分代假说(weak generational hypothesis):绝大多数对象存活时间很短(很快变成垃圾),而少数对象存活时间较长。据此把堆分为新生代(Young)与老年代(Old),新生代对象快速分配、频繁回收,老年代对象存活久、回收少。Minor GC(新生代回收)在新生代空间(如 Eden/Survivor)被填满、无法分配新对象时触发,只回收新生代,速度快、停顿小,存活对象晋升到老年代。Full GC(整堆回收)在老年代也被填满、或 CMS 类并发回收失败、或需要整堆压缩/元空间不足时触发,回收整个堆,通常伴随较长的停顿。因此触发条件根本区别在于"新生代还是老年代内存不足"。

弱分代假说使得"只回收新生代"能以极低代价回收大部分垃圾,从而以 Minor GC 的频繁小停顿替代全局 Full GC;Full GC 因涉及老年代与元空间,代价高,故触发条件更严格。

#
★★★

2. G1 把堆划分为 region 并优先回收垃圾最多的区域,它相对 CMS 在停顿可控性上有何改进?

G1 把堆划分为 region 并优先回收垃圾最多的区域,它相对 CMS 在停顿可控性上有何改进?

  • G1 的 region 划分与并发回收
  • 优先回收垃圾最多的 region(Garbage First)
  • 停顿可预测(可设定停顿目标)

G1 把堆划分为多个大小相等的 region,并跟踪每个 region 的垃圾占比,回收时优先回收垃圾最多(Garbage First)的 region,从而以有限工作换取最大收益。G1 采用并发标记 + 增量回收,通过 RSet(remembered set)维护跨 region 引用,可以只回收部分 region 而不必整堆,因此停顿可控。相比 CMS,G1 提供可预测的停顿目标(-XX:MaxGCPauseMillis),通过动态调整回收量与 region 选择来尽量满足停顿时间预算,且不存在 CMS 的碎片化与并发失败不可控问题;G1 还具备可选的整堆压缩能力,避免内存碎片导致的失败。这使 G1 在停顿可控性与吞吐量之间取得更均衡的取舍。

CMS 的毛病是并发标记失败时会退化为串行 Full GC 且停顿不可控、易碎片化。G1 通过 region 化 + RSet + 垃圾优先回收 + 停顿目标设置,把停顿从"不可控"变成"可预算",是实现低延迟可预测的关键改进。

#
★★★

3. ZGC 借助染色指针与读屏障实现并发整理,为什么能把停顿控制在亚毫秒级?

ZGC 借助染色指针与读屏障实现并发整理,为什么能把停顿控制在亚毫秒级?

  • 染色指针存储状态与转发信息
  • 读屏障在访问时重定位
  • 并发整理 + 极小 STW

ZGC 使用染色指针(colored pointer):在 64 位指针的冗余位中编码对象状态(如 Marked0/Marked1/Remapped/Finalizable 等),无需额外字段即可随指针携带状态。它通过读屏障(read barrier)在每次对象访问时检查指针状态:若对象需要重定位(被移动),读屏障会把访问重定向到转发后的新地址,从而让对象并发移动成为可能,应用线程无需停下等待整理。ZGC 的标记、重分配、重映射等大部分阶段都与用户线程并发进行,只有极少数阶段(如根扫描、GC 开始时)需要短暂停止,且这些停顿与堆大小基本无关。因此 ZGC 能把 STW 停顿压缩到亚毫秒级。

染色指针把状态"塞进指针本体"省去额外内存开销,读屏障在访问时廉价地完成重定位,使对象搬移与重映射可并发,从而把停顿从"打扫堆"降为"只停根扫描等极短操作",实现亚毫秒停顿。

#
★★★

4. safepoint 是 JVM 能让线程停下进入 STW 的安全点,为什么 GC、偏向锁撤销、线程 dump 都依赖它?

safepoint 是 JVM 能让线程停下进入 STW 的安全点,为什么 GC、偏向锁撤销、线程 dump 都依赖它?

  • safepoint 是线程可安全停止执行的点
  • 所有线程在 safepoint 同步进入 STW
  • GC、偏向锁撤销、线程 dump 需要全局一致状态

safepoint 是 JVM 中所有线程都"能安全停下来并进入全局一致状态"的检查点。因为 JVM 需要一种全局同步机制:在 safepoint 处,所有线程(包括运行中的线程、JIT 编译线程、JIT 带代码的线程)都会到达并暂停,此时 JVM 处于一个可安全操作堆/栈/锁的稳定状态。GC需要扫描堆与栈、移动对象,必须所有线程停下来才能保证引用一致;偏向锁撤销需要修改对象头与撤销偏向,若无全部线程停顿,可能有人正在使用该锁;线程 dump 需要抓取所有线程一致的栈与执行状态。这些操作都要求"堆/锁/栈的全局一致快照",因此都依赖 safepoint 让所有线程同步停下进入 STW。

safepoint 是 JVM 的"全局同步点":它把"所有线程停顿"抽象为统一机制,任何需要全局一致状态的操作(GC、偏向锁撤销、线程 dump、类重定义等)都可复用该机制,避免各自实现复杂的线程协作。

#
★★★

5. 写屏障在引用赋值时插入额外代码,它在分代 GC 中如何维护卡表以追踪跨代引用?

写屏障在引用赋值时插入额外代码,它在分代 GC 中如何维护卡表以追踪跨代引用?

  • 写屏障在赋值时执行
  • 卡表(card table)记录脏页
  • 跨代引用追踪

写屏障(write barrier)在每次引用赋值(把对象 A 的某字段指向对象 B)时插入额外代码。分代 GC 中,为了让 Minor GC 不必扫描整个老年代就能找到所有"老年代指向新生代"的跨代引用,用**卡表(card table)**记录:每个卡(对应一块堆内存区域)标记是否可能包含指向新生代的引用。写屏障在赋值时检查:若写入的引用(B)位于新生代、而写的位置(A 在某对象)位于老年代,就把该卡标记为"脏(dirty)"。Minor GC 时只需扫描脏卡对应的老年代区域,找出并记录跨代引用,作为新生代的可达根,从而避免全堆扫描。

写屏障 + 卡表是"用写时维护成本换取扫描时减少成本"的经典取舍:写屏障在赋值时记录脏卡,使 Minor GC 能只扫脏卡区域定位跨代引用,极大降低停顿,是分代 GC 正确性与效率的关键。

#
★★★

6. 偏向锁撤销为何需要在 safepoint 进行?JDK 15 起默认禁用偏向锁的原因是什么?

偏向锁撤销为何需要在 safepoint 进行?JDK 15 起默认禁用偏向锁的原因是什么?

  • 偏向锁撤销需要安全修改锁归属
  • 需在 safepoint 全局停顿
  • 高版本默认禁用的原因(撤销代价、收益小)

偏向锁撤销时,需要把对象头中的偏向标记与实际持有者信息清除/改写,并可能与其他线程的同步操作协调。由于存在多个线程可能同时持有或竞争锁,撤销必须保证没有其他线程正在使用该锁对象,因此 JVM 在 safepoint 处让所有线程停止,从而在全局一致状态下安全地改写对象头、撤销偏向,避免竞态。JDK 15 起默认禁用偏向锁,是因为在现代应用(尤其是多线程竞争频繁、与 JVM 其他特性如可伸缩性、少锁竞争场景)下,偏向锁的收益(单线程重复获取锁的开销节省)已不明显,而撤销偏向锁需要在 safepoint 停顿、涉及全局协作,代价较高,且与一些新特性(如虚拟线程、GC 优化)存在相互作用,故整体收益不如负担,于是默认关闭。

偏向锁撤销的"全局同步"属性使其停靠 safepoint;而现代多核高并发场景下偏向锁收益递减、撤销代价(safepoint 停顿)相对突出,故高版本默认禁用,反映"优化收益随硬件与场景演变"的取舍。

#
★★

7. LLVM IR 采用 SSA 形式与强类型系统,这种设计给跨语言优化和静态分析带来了什么便利?

LLVM IR 采用 SSA 形式与强类型系统,这种设计给跨语言优化和静态分析带来了什么便利?

  • SSA 的 def-use 与数据流便利
  • 强类型系统的类型信息
  • 跨语言统一 IR 的接口

LLVM IR 的 SSA 形式使每个值有唯一定义点,def-use 链清晰,优化器可直接沿 def-use 传播常量、做 GVN、死代码消除,无需复杂的到达定义分析,简化了数据流优化。强类型系统(带类型标注的指令、明确的指针/整数/浮点/向量类型)给静态分析提供类型信息,使别名分析、类型检查、内存模型分析更精确,也便于中间层做类型相关的变换。同时,LLVM IR 作为目标无关的统一中间表示,把 C、C++、Rust、Swift 等不同语言前端产出的程序规范化为同一 IR,使共享的优化器与后端可跨语言复用,并让跨语言优化(如混合内联、统一的分析)成为可能,极大提升工具链的复用性。

SSA 简化数据流、强类型增强分析精度,统一 IR 则让多语言共享同一套优化与后端,三者共同构成 LLVM 作为跨语言编译基础设施的便利性基础。

#
★★

8. LLVM 新 Pass Manager 相比旧版在 Pass 调度、IR 单元(module/function/loop)粒度与结果缓存上有何改进?

LLVM 新 Pass Manager 相比旧版在 Pass 调度、IR 单元(module/function/loop)粒度与结果缓存上有何改进?

  • Pass 调度的显式与可预测
  • 统一的 IR 单元粒度(module/function/loop)
  • analysis 结果缓存与失效

新 Pass Manager 相比旧版有多处改进:调度上,旧版依赖隐式深度优先遍历与 Pass 自管理依赖,顺序性差、难以预测;新版提供显式构造的 PassPipeline,调度顺序可复现、可指定。IR 单元粒度上,旧版把 function pass 与 module pass 分开管理、跨函数状态难传递;新版用统一的 AnalysisManager 以 module/function/loop 为粒度组织 Pass 与 analysis,支持 loop 级 pass 与跨粒度信息共享。结果缓存上,旧版 analysis 结果常被重复计算或失效不精确;新版统一缓存 analysis 结果,Pass 通过 PreservedAnalyses 声明保留哪些分析,失效只发生在相关分析上,避免冗余重算,也减少了因隐式依赖导致的正确性风险。

新 Pass Manager 的改进本质是把"调度、粒度、缓存"从各 Pass 的隐式约定提升为统一基础设施,使 pipeline 可预测、结果可复用、失效精确,是近版本 LLVM 优化管线工程化的核心。

#
★★

9. eBPF 后端如何把 LLVM IR 编译为 BPF 字节码,verifier 的约束(循环、指针)如何反向影响代码生成?

eBPF 后端如何把 LLVM IR 编译为 BPF 字节码,verifier 的约束(循环、指针)如何反向影响代码生成?

  • LLVM 的 BPF 后端(Target/BPF)
  • 指令选择与合法化
  • verifier 约束(有界循环、指针算术限制)反向影响生成

LLVM 提供 BPF 后端(Target/BPF),它把经过优化后的 LLVM IR 通过指令选择、寄存器分配等步骤编译为 BPF 字节码(eBPF 指令集),并可用 llvm-objdump 等查看。但由于内核 eBPF verifier 会静态检查字节码,代码生成必须满足 verifier 的约束:例如循环必须有界(verifier 要求循环可被证明终止,否则拒绝),指针算术必须安全(指针只能与标量相加、不能做任意的指针运算、访问必须可证明在范围内),因此 LLVM 后端/优化 Pass 会受此约束反向影响——不能生成无限循环或危险指针运算的代码,必要时会通过展开循环、限制指针变换来满足 verifier 检查,否则加载会被内核拒绝。

eBPF 的"编译与验证"是闭环:LLVM 生成字节码后还要过 verifier 的静态安全检查,后端不能只求执行正确,还要保证生成代码可被 verifier 接受,因此 verifier 的安全策略反向约束了代码生成与优化。

#
★★

10. 别名分析(Alias Analysis)如何决定内存相关优化能否进行,noalias 与 restrict 对向量化的意义是什么?

别名分析(Alias Analysis)如何决定内存相关优化能否进行,noalias 与 restrict 对向量化的意义是什么?

  • 别名分析判定访存是否可能冲突
  • noalias 允许更自由的重排与向量化
  • restrict 承诺无别名

别名分析决定两个内存访问是否可能指向同一地址,从而决定内存相关优化能否进行:若证明两个访问不别名(no-alias),优化器可安全地重排、消除、向量化这些访问;若保守地 may-alias,则必须假设访问顺序/冲突,取消相关优化。noalias表示两个指针区域不重叠,是编译器可安全做并行/向量化的依据。restrict(C 之 restrict 关键字,LLVM 中对应 noalias 属性)是程序员/前端向编译器承诺"该指针访问的区域不与任何其他指针重叠",编译器据此可把对多元素的循环向量化(用 SIMD 同时处理多个元素),因为可以自由重排与合并访存而不担心冲突。若 restrict 承诺被违反,则产生未定义行为,优化可能错误。

别名分析是访存优化的"准入门票":noalias/restrict 提供"无别名"的强保证,使向量化与访存重排成为可能,是利用已知无别名信息换取并行度与吞吐的关键。

#
★★

11. JIT 与 AOT 在启动延迟、峰值性能与运行时内存占用上如何取舍,分层编译如何在其间折中?

JIT 与 AOT 在启动延迟、峰值性能与运行时内存占用上如何取舍,分层编译如何在其间折中?

  • JIT 启动快但需运行时编译、内存占用高
  • AOT 启动慢/提前编译但峰值稳定
  • 分层编译折中

JIT在运行时编译,程序启动后需先解释/慢速执行并逐步优化预热,达到峰值性能的启动预热延迟相对较长,且编译发生在运行期,占用运行时内存与 CPU;AOT在部署前编译,启动时直接加载原生代码,启动快、无运行时编译开销,但占用更大的二进制体积、且无法利用运行期反馈做针对性优化,峰值性能可能不如充分预热后的 JIT。分层编译在两者间折中:先用快编译器(如解释器 + 快速 JIT)让程序快速启动,再对运行期采集的热代码用更高级的 JIT 逐步优化,从而在启动延迟、峰值性能与运行时内存/编译开销之间自适应权衡——冷代码用低开销路径,热代码逐步提升到高性能路径。

分层编译把"启动快"与"峰值高"这对矛盾通过"按热度分级编译"统一起来:把编译成本分配到热代码上,实现无需全量 AOT 也能获得接近 AOT 的稳态性能,同时保持低启动延迟。

#
★★

12. LLVM 的 attribute(如 noundef、nonnull、sret)如何承载语义信息并驱动优化与验证?

LLVM 的 attribute(如 noundef、nonnull、sret)如何承载语义信息并驱动优化与验证?

  • attribute 编码函数/参数/返回语义
  • noundef、nonnull、sret 的含义
  • 驱动优化与验证

LLVM 的 attribute 是附加在函数、参数、返回值上的语义声明,编码调用者必须遵守的契约。noundef表示参数/返回值不会是 undef/poison,优化器可安全依赖其值;nonnull表示指针参数/返回值非空,优化器可据此消除空指针检查(如 null 分支);sret(struct return)表示该参数用于返回结构体,驱动特定的 ABI 约定与调用约定。这些属性让优化器基于更精确的假设做折叠、消除、传播等变换,同时也可用于验证(如属性冲突检测、调用者是否遵守契约)。attribute 是"编译器与调用者间契约"的载体,正确标注提升优化,错误标注则可能导致未定义行为。

attribute 把"程序员/前端已知的语义约束"编码进 IR,使优化器能利用这些约束做更强的假设与变换,其价值在于"用契约换取优化空间",同时通过契约一致性来保证正确性。

#
★★

13. 为何 eBPF verifier 要求有界循环与可证明的内存访问,这使 LLVM 的循环优化在该后端受到哪些限制?

为何 eBPF verifier 要求有界循环与可证明的内存访问,这使 LLVM 的循环优化在该后端受到哪些限制?

  • verifier 需静态证明终止与内存安全
  • 有界循环要求避免无限循环
  • 可证明内存访问避免越界

内核 eBPF verifier 会对加载的字节码做静态分析,必须证明循环有界(避免无限循环导致内核被占住)和内存访问可证明在界内(避免越界破坏内核内存)。因此它需要进行行数/状态有限的验证,若不满足则拒绝加载。这反向限制了 LLVM 的循环优化:循环展开(unrolling)可能产生大量指令导致 verifier 指令数超限而失败;循环不变量外提等带来更复杂控制流的变换可能使 verifier 难以证明有界;涉及大型或动态间接内存访问的变换可能使 verifier 无法证明访问安全。因此 eBPF 后端常常需要避免过度的循环展开或简化循环结构,使生成代码满足 verifier 的"有界 + 可证明"约束。

verifier 的"可证明性"要求是 eBPF 安全模型的基石,它反过来约束了代码生成:LLVM 后端不能在 eBPF 上做那些"会破坏可证明性/增加指令数"的激进循环优化,必须在优化与 verifier 接受度之间取舍。

#
★★

14. counted loop 默认不插 safepoint poll,为什么会导致 GC 长时间无法 STW?UseCountedLoopSafepoints 如何缓解?

counted loop 默认不插 safepoint poll,为什么会导致 GC 长时间无法 STW?UseCountedLoopSafepoints 如何缓解?

  • safepoint poll 是线程可安全停止的检查点
  • counted loop 无调用点则无 poll
  • 长期无 poll 导致无法 STW

safepoint 是线程能让 JVM 进入 STW 的检查点,通常设在函数调用点、回边等。counted loop(计数器控制的循环)若内部没有函数调用(无调用点),默认可能不在循环体内部插入 safepoint poll,那么线程在循环内长时间运行期间就没有任何 safepoint 检查点,JVM 无法在该线程处安全停止,导致 GC 无法执行 STW(或需要等待该线程退出循环),出现长时间停顿。UseCountedLoopSafepoints 选项/机制会在 counted loop 的循环体内(如回边、迭代处)插入 safepoint poll,使线程在循环迭代时能周期性到达 safepoint,从而让 GC 能在合理时间内进入 STW,避免因长循环无检查点导致的长时间停顿。

safepoint 的"覆盖"取决于线程能否频繁到达检查点:无调用的长循环会形成"无 poll 的窗口",破坏 STW 的及时性;在循环内插 poll 是补上检查点、保证 GC 可及时停顿的关键缓解。

#
★★

15. 并发标记阶段用户线程仍在修改引用,写屏障如何配合 SATB 或增量更新保证标记正确?

并发标记阶段用户线程仍在修改引用,写屏障如何配合 SATB 或增量更新保证标记正确?

  • 并发标记的漏标问题
  • SATB(Snapshot At The Beginning)记录起始快照
  • 增量更新(Dirty Card)记录新引用

并发标记阶段用户线程仍在运行并修改引用,若不处理,可能把"已标记对象到新存活对象的引用"丢失,导致漏标(把存活对象当垃圾回收)。为此写屏障承担保护作用,配合两种策略:**SATB(Snapshot At The Beginning)**在标记开始前记录快照,写屏障在改写引用时把被覆盖的旧引用(旧值)记录到缓冲区,标记时按起始快照处理,从而保证起始时存活的对象不会被漏标;增量更新(Incremental Update) 则是在写屏障记录新写入的引用(新值),后续重新扫描该引用以标记被新引用的对象。两种方式都通过写屏障捕获"标记期间发生的引用变化",补上并发修改造成的漏标缺口,保证标记正确。

并发标记的难点是"标记与修改并发"可能漏掉存活对象;SATB 通过保存起始快照、增量更新通过保留新引用,配合写屏障在引用变化时记录,从而保证"不再被标记的存活对象"被识别,维持标记的完备性。

#
★★

16. STW 停顿与吞吐量的权衡如何影响 GC 选型?低延迟服务为何倾向 ZGC/Shenandoah?

STW 停顿与吞吐量的权衡如何影响 GC 选型?低延迟服务为何倾向 ZGC/Shenandoah?

  • STW 停顿 vs 吞吐量的权衡
  • 低延迟服务对停顿敏感
  • ZGC/Shenandoah 的并发低停顿

GC 选型面临停顿(latency)与吞吐量(throughput)的权衡:吞吐量优先的 GC(如 Parallel GC)通过较少但可能较长的 STW 停顿换取高吞吐,适合批处理;停顿优先的 GC 通过更多并发/增量工作换取更短、更可控的停顿,代价是吞吐略降与额外内存开销。低延迟服务(如在线响应、交易、网关)对单次停顿(尤其在 GC 慢路径、长停顿导致请求超时)极为敏感,因此倾向选择ZGC / Shenandoah 这类可并发整理、把 STW 压到亚毫秒/毫秒级、且停顿基本与堆大小无关的 GC,以换取稳定的低延迟与可预测性,即使牺牲一部分吞吐与内存。

低延迟服务把"停顿时间"视为最关键指标,宁可牺牲吞吐换取停顿可预测;ZGC/Shenandoah 通过并发标记、并发整理、染色指针/读屏障等把 STW 压到极小,故是低延迟场景的首选。

#

17. 三色标记中漏标由哪两个条件共同导致?写屏障如何破坏其中一个条件?

三色标记中漏标由哪两个条件共同导致?写屏障如何破坏其中一个条件?

  • 三色标记(白/灰/黑)与漏标条件
  • 条件一:黑色对象指向白色对象
  • 条件二:黑色对象已不再被扫描(灰色对象丢失该引用)

三色标记中,漏标(把存活对象当垃圾回收)由两个条件同时满足导致:条件一是某个黑色对象(已扫描完)在这之后被写入指针指向一个白色对象(未标记);条件二是原本持有该白色对象引用的灰色对象(尚未扫描)的这条引用被删除/改写,导致该白色对象不再有任何可达路径被扫描。两者同时发生时,黑色对象不会再被扫描,而白色对象又失去其他引用来源,于是被漏标。写屏障破坏其中一个条件:或阻止黑色对象被写入新引用(增量更新,记录新引用使其重新被扫描/标记),或保存被覆盖的旧引用(SATB,把旧引用作为灰色对象继续处理),从而保证漏标不会发生。

不漏标的关键是"黑色对象不能再指向未标记的白色对象"或"被覆盖的引用仍被标记"。写屏障通过在写时记录新值或旧值,破坏上述两个条件之一,从而在并发修改下保证标记完备。

#

18. 内联(inlining)、GVN、循环不变量外提(LICM)等 Pass 各自消除什么冗余,触发条件与代价模型是什么?

内联(inlining)、GVN、循环不变量外提(LICM)等 Pass 各自消除什么冗余,触发条件与代价模型是什么?

  • 内联消除函数调用开销
  • GVN 消除重复计算
  • LICM 消除循环内重复计算

三个 Pass 消除不同冗余:**内联(inlining)**把被调函数体复制到调用点,消除调用开销(call/return、参数传递、保存寄存器)并打开跨函数优化窗口,由代价模型(函数体大小、调用频率、收益)决定是否内联;GVN(包括全局公共子表达式消除)消除对同一表达式的重复计算,通过值编号检测重复并复用,对"操作数相同且未被改写"的表达式生效;**LICM(循环不变量外提)**把循环内不随迭代变化的值/计算移到循环外,消除每次迭代的重复工作,触发条件是"表达式在循环内不依赖循环变量且操作数在循环内不被改写"。三者的代价模型都权衡"优化收益"与"代码膨胀/编译时间/寄存压力"。

内联消除控制流开销、GVN 消除数据流重复、LICM 消除时间性重复,三者都是"用更多代码/更早计算换取更少重复执行",其共同点是依赖代价模型与精确数据流分析(活域、别名、可用性)来决定是否值得及是否安全。

#

19. Pass 之间的依赖与失效(analysis invalidation)如何被 Pass Manager 管理,为何顺序会影响优化结果?

Pass 之间的依赖与失效(analysis invalidation)如何被 Pass Manager 管理,为何顺序会影响优化结果?

  • Pass 声明依赖的 analysis
  • PreservedAnalyses 与失效传播
  • 顺序影响结果的机制

Pass Manager 管理 Pass 之间的依赖与失效:每个 Pass 声明它需要哪些 analysis(依赖),Pass Manager 保证在运行该 Pass 前缓存/计算出所需 analysis;Pass 运行后通过返回 PreservedAnalyses声明它保留了哪些 analysis(未修改其依赖的基础),Pass Manager 据此判断哪些 analysis 失效需要重建、哪些可复用。这样避免重复计算并保证正确性。顺序会影响优化结果,因为优化是"逐步变换":同一分析在不同阶段看到的 IR 不同,先做 A 再做 B 可能让 B 的收益更大(如先内联再 GVN 能消除更多冗余),反过来则可能不同;且前一个 Pass 可能使某个 analysis 失效,改变后续 Pass 可用的信息。因此 pipeline 顺序决定可获得的优化组合与最终代码质量。

Pass Manager 的依赖/失效管理让"分析结果在正确时复用、在修改时失效",保证正确性;而顺序影响结果是"优化变换的先后决定可消除的冗余形态",是 Pass pipeline 设计(如 -O2 的固定顺序)的核心考量。