LLVM 与优化:SSA、poison 与 pass pipeline¶
LLVM IR 位于源语言与目标机器之间:它低到可以表达地址、整数宽度、原子序和调用约定,高到仍保留无限虚拟寄存器、类型、控制流与 metadata。它不是可移植汇编,也不是“更安全的 C”;IR 自身有精确而强的未定义行为与 poison 规则,优化合法性依赖这些语义。
本文固定 LLVM 21.1.0 发布文档作为可重现基准。主站 llvm.org/docs 在 2026-07-28 对应后续开发分支,IR 与 pass 名称可能已经不同;实验应始终配套 opt --version 与发布版 LangRef。
三种等价载体¶
LLVM code representation 可存在为:
- in-memory IR.
- bitcode;
- human-readable
.llassembly。
文本与 bitcode 适合工具交换,但跨 LLVM 大版本的兼容承诺需要按 bitcode policy 核验;内部 C++ API 更不是稳定 ABI。
每个 %x 是 SSA value,不代表一个固定机器寄存器。后端可消除它、常量折叠、复用寄存器或把值 spill 到内存。
Module、DataLayout 与 target¶
Module 包含 function、global、alias、metadata 和 target information。相同 IR 在不同 target 上可能有不同合法布局:
target triple = "x86_64-unknown-linux-gnu"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
DataLayout 描述 endianness、pointer/address space、ABI/preferred alignment、native integer width 与 stack alignment。生成 IR 的前端不能凭宿主 sizeof 猜目标;cross compilation 时 host 与 target 明确分离。
opaque pointer 之后,普通 pointer 类型不再编码 pointee type;load/store/call 自己携带访问类型。类型正确不等于 provenance、对齐或对象生命周期正确。
SSA 与控制流¶
基本块由 label 开始,以 terminator 结束。\(\phi\) 的 incoming value 与 predecessor 一一对应:
define i32 @abs(i32 %x) {
entry:
%neg = icmp slt i32 %x, 0
br i1 %neg, label %minus, label %plus
minus:
%n = sub i32 0, %x
br label %join
plus:
br label %join
join:
%r = phi i32 [ %n, %minus ], [ %x, %plus ]
ret i32 %r
}
这段 IR 对最小负数的行为取决于 sub flags:没有 nsw 时按固定位宽整数运算;加上 sub nsw 后,signed overflow 产生 poison。前端必须准确翻译源语言的溢出规则。
内存操作不服从“每地址只写一次”。LLVM 用 alias analysis、MemorySSA、capture/provenance、function attribute 和 lifetime marker 为内存变换提供证明。
undef、poison、UB 与 freeze¶
这四个概念不能混用:
Immediate UB¶
执行到某些操作会使整个执行没有语义约束,例如违反 LangRef 规定的内存访问、执行 unreachable、某些除零或调用契约错误。优化器可假设定义良好的程序不会走到这里。
Poison¶
poison 是延迟触发的坏值,可由 nsw/nuw/exact 等违反承诺产生。它会沿许多运算传播;一旦在分支条件、内存地址等触发位置使用,可能产生 UB。
%x = add nsw i32 2147483647, 1 ; poison
%y = icmp eq i32 %x, 0 ; poison
br i1 %y, label %a, label %b ; UB
undef¶
每次使用 undef 可选择任意允许值,不应把它当作“某个未知但固定的变量”。LangRef 建议在可能时用 poison 表示未定义计算。
freeze¶
freeze 把 poison/undef 变成一次选择后对该结果所有使用保持固定的任意值:
这不会恢复源程序意图,只阻止坏值继续以某些方式传播。任何手写 IR、instrumentation 或 pass 都必须理解这组规则,否则“看起来等价”的变换可能错误。
属性与 metadata 是优化契约¶
例如:
nonnull、dereferenceable、align描述 pointer;noalias描述调用边界的别名承诺;readonly/memory(...)描述内存效应;nounwind、willreturn、mustprogress描述控制流;range、TBAA、alias scope 提供值或别名信息;- debug metadata 保持源码映射。
属性不是装饰。前端若错误标注 nonnull,用户传 null 时可能不是“检查失效”,而是整个调用进入 poison/UB 语义,导致周围控制流被删除。
metadata 的语义强度不同:debug info 丢失通常不改变程序语义;某些 optimization metadata 则影响变换但有特定 drop 规则。写 pass 时要按 LangRef 逐项处理,不能盲目复制全部 metadata。
Pass:analysis、transform 与 invalidation¶
analysis 计算 dominator tree、loop info、alias、scalar evolution 等事实;transform 修改 IR。修改后,旧 analysis 是否仍有效必须明确:
IR before
-> AnalysisManager caches DominatorTree / LoopInfo
-> Transform changes CFG
-> preserve exact valid analyses
-> invalidate the rest
New Pass Manager 以 analysis manager 和 preserved analyses 管理缓存。错误宣称“preserved”会让后续 pass 使用陈旧事实,产生误编译;全部 invalidate 虽正确,却损失编译性能。
最小 function pass 骨架:
#include <llvm/IR/Function.h>
#include <llvm/IR/PassManager.h>
#include <llvm/Support/raw_ostream.h>
struct CountBlocksPass : llvm::PassInfoMixin<CountBlocksPass> {
llvm::PreservedAnalyses run(
llvm::Function& f,
llvm::FunctionAnalysisManager&) {
llvm::errs() << f.getName() << ": " << f.size() << "\n";
return llvm::PreservedAnalyses::all();
}
};
该 pass 只读 IR,所以可保留全部 analysis。若删改 instruction、edge 或 attribute,返回集合必须重新论证。
默认优化流水线¶
-O0 到 -O3、-Os、-Oz 是 driver/pipeline 选择,不属于 LLVM IR 语义。具体 pass 顺序会随 LLVM 版本与 target 改变。概念上需要反复:
- canonicalize CFG、loops、aggregates;
- expose constants、known bits、ranges;
- inline/devirtualize 以扩大局部视野;
- eliminate dead/redundant work;
- vectorize/unroll 并权衡 code size;
- lower 到 target legality;
- machine optimization、register allocation、scheduling。
pass 不是彼此独立。Inlining 可能暴露常量,随后 SROA 和 DCE 消除对象;也可能扩大函数导致 I-cache miss 与 register spill。观察中间 IR 比只看最终汇编更容易定位因果。
opt -S -passes='default<O2>' input.ll -o output.ll
opt -S -passes='print<domtree>' input.ll -disable-output
opt -S -passes='loop-simplify,licm' input.ll -o licm.ll
用 -print-pipeline-passes、debug pass manager 或固定版文档核实 syntax;不要把博客中的旧 legacy pass 参数照搬到新版本。
Loop 与 vectorization¶
LLVM 常先把循环正规化,建立 preheader、single latch、dedicated exit 等结构。ScalarEvolution 用递推表达 induction 与 trip count;Loop Vectorizer/SLP 再依据依赖与成本模型向 SIMD 映射。
向量化合法性需要证明:
- 迭代间没有破坏语义的 memory dependence;
- reduction 顺序在整数/浮点规则下可改变;
- runtime alias check 能覆盖未知关系;
- remainder/epilogue 处理边界;
- 对齐、masked access 与 target instruction 合法。
浮点优化尤其敏感。fast-math flags 分别放宽 NaN、Inf、signed zero、reassociation 等语义,不能用一个“CPU 会更快”理由全开。数值稳定性和可复现性也属于正确性契约。
读 optimization remark¶
clang -O3 -Rpass=loop-vectorize \
-Rpass-missed=loop-vectorize \
-fsave-optimization-record demo.c -c
remark 可区分 Passed、Missed、Analysis,并携带 source location、pass 与原因。它解释编译器判断,不证明目标 workload 真的加速。
LTO、ThinLTO 与 PGO¶
普通分离编译只在 translation unit 内优化;LTO 把 IR 延迟到链接期,能跨单元 inline、devirtualize 和 internalize。Full LTO 视野更集中,资源消耗大;ThinLTO 用 summary index 与分布式 backend 平衡跨单元分析和并行。
PGO 提供实际执行频率:
- instrumentation PGO 先插桩、运行代表 workload、再优化;
- sample PGO 从 profile sample 映射回源码/IR;
- context-sensitive profile 可区分调用上下文。
错误 profile 的风险:
- 训练流量不覆盖生产热路径;
- binary/source mismatch 令映射失真;
- 稀有错误路径被过度冷化;
- 优化后 layout 改变 profile 采集偏差;
- profile 合并掩盖不同机器/租户。
保留 profile provenance、覆盖率和版本,并同时做无 PGO 对照。
验证与误编译排查¶
IR verifier 检查结构不变量,不证明语义等价。Alive2 可对一部分 LLVM IR transformation 做 translation validation;随机 differential testing 与 reducer 则寻找不同编译配置的输出分歧。
排查顺序:
- 固定源码、输入、compiler commit、target 与 flags;
- 用 sanitizer/语言规则排除源 UB;
- 比较不同优化级别与 backend;
- 保存 preprocessed source 或 bitcode;
- 二分 pass/commit;
- 用
llvm-reduce保留错误谓词最小化。
性能与边界¶
- IR instruction count 不等于机器 work;
- “消除了 load”可能增加 register pressure 与 spill;
- unroll/vectorize 可能伤害 I-cache 和 tail;
- LTO 改变 symbol visibility、debugging、incremental build 与 link memory;
- sanitizer build 改变 layout 和 timing,适合找错而非性能基线;
-march=native绑定构建机特性,不适合未知部署机器;- hand-written IR 必须固定 LLVM 语义版本并通过 verifier。
继续阅读¶
- 前端与后端全景:见编译器流水线。
- LTO 之后如何形成 ELF:见链接、加载、ELF 与 ABI。
- 检验优化收益:见基准设计与采样分析。
Reference¶
- LLVM 21.1.0 Language Reference Manual
- LLVM 21.1.0 Undefined Behavior Manual
- LLVM 21.1.0 New Pass Manager
- LLVM 21.1.0 Analysis and Transform Passes
- LLVM 21.1.0 Vectorizers
- LLVM 21.1.0 Optimization Remarks
- Clang 21.1.0 ThinLTO
- LLVM 21.1.0 PGO
- Alive2: Bounded translation validation for LLVM
- Finding and Understanding Bugs in C Compilers