强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

LLVM 开发指南 / 第 8 章:优化管线

第 8 章:优化管线

“编译器优化不是魔法,而是对程序变换的系统性应用。”


8.1 优化级别详解

8.1.1 优化级别对照表

级别 Flag 优化策略 编译速度 运行速度 代码大小
O0 -O0 无优化 ⚡⚡⚡⚡ 🐢 📦📦📦📦
O1 -O1 基础优化 ⚡⚡⚡ 🏃🏃 📦📦📦
O2 -O2 标准优化 ⚡⚡ 🚀🚀 📦📦
O3 -O3 激进优化 🚀🚀🚀 📦📦📦
Os -Os 优化大小 ⚡⚡ 🏃🏃 📦
Oz -Oz 最小大小 ⚡⚡ 🏃 📦

8.1.2 各级别启用的主要 Pass

# 查看各优化级别启用的 Pass
opt -O0 -print-pipeline-passes input.ll -o /dev/null 2>&1 | head -20
opt -O2 -print-pipeline-passes input.ll -o /dev/null 2>&1 | head -50
opt -O3 -print-pipeline-passes input.ll -o /dev/null 2>&1 | head -80
优化技术 O1 O2 O3 Os Oz
SimplifyCFG
InstCombine
SROA
Inliner ✅激进
GVN
LoopVectorize
SLPVectorize
LoopUnroll ✅激进 ✅保守
LoopInterchange
LoopDistribute
CallSiteSplitting

8.2 函数内联 (Function Inlining)

函数内联是最重要的优化之一,它可以消除函数调用开销,并为后续优化创造机会。

8.2.1 内联的基本原理

内联前:                      内联后:
┌──────────────┐            ┌──────────────────┐
│ main()       │            │ main()           │
│  ...         │            │  ...             │
│  foo(x)      │     ──►    │  { // foo 的代码 │
│  ...         │            │    a = x * 2;    │
└──────────────┘            │    b = a + 1;    │
                            │  }               │
┌──────────────┐            │  ...             │
│ foo(int x)   │            └──────────────────┘
│  a = x * 2;  │
│  b = a + 1;  │
│  return b;   │
└──────────────┘

8.2.2 内联决策

// Clang 中控制内联的属性
__attribute__((noinline)) void func_no_inline();     // 禁止内联
__attribute__((always_inline)) void func_always();   // 强制内联

// C++17 属性
[[gnu::always_inline]] void func();
[[gnu::noinline]] void func();
# 控制内联的命令行选项
clang -mllvm -inline-threshold=200 file.c    # 调整内联阈值
clang -fno-inline file.c                      # 禁止内联
clang -mllvm -inlinehint-threshold=500 file.c # hint 函数阈值

8.2.3 内联成本模型

LLVM 的内联器使用成本模型决定是否内联:

因素 影响
基本块数量 越多越贵
指令数量 越多越贵
调用深度 深度调用链降低内联概率
是否热路径 Profile 信息影响
函数属性 always_inline 强制内联
是否有循环 有循环的函数成本更高

8.2.4 内联的影响

// 内联前:优化器看不到跨函数信息
int square(int x) { return x * x; }
int compute() { return square(42); }  // 内联后可以直接优化为 1764

// 内联后:
int compute() { return 42 * 42; }     // 常量折叠
// 进一步优化为:
int compute() { return 1764; }

8.3 常量传播与折叠

8.3.1 稀疏条件常量传播 (SCCP)

; 优化前
define i32 @test() {
entry:
  %x = add i32 1, 2          ; x = 3
  %y = mul i32 %x, 4         ; y = 12
  %z = icmp sgt i32 %y, 10   ; z = true
  br i1 %z, label %if, label %else

if:
  ret i32 %y                 ; 一定返回 12

else:
  ret i32 0                  ; 死代码,永远不会执行
}

; 优化后
define i32 @test() {
entry:
  ret i32 12
}

8.3.2 全局值编号 (GVN)

; 优化前 — 冗余计算
define i32 @redundant(i32 %a, i32 %b) {
entry:
  %x = add i32 %a, %b
  %y = add i32 %a, %b       ; 与 %x 相同!
  %z = mul i32 %x, %y
  ret i32 %z
}

; 优化后 — GVN 消除冗余
define i32 @redundant(i32 %a, i32 %b) {
entry:
  %x = add i32 %a, %b
  %z = mul i32 %x, %x
  ret i32 %z
}

8.4 向量化优化

8.4.1 自动向量化概述

LLVM 有两种自动向量化:

类型 说明 适用场景
Loop Vectorizer 循环向量化 数组运算、循环
SLP Vectorizer 超字级并行 连续独立运算

8.4.2 循环向量化

// C 源码
void add_arrays(float *a, float *b, float *c, int n) {
    for (int i = 0; i < n; i++) {
        c[i] = a[i] + b[i];
    }
}
; 向量化前(标量循环)
loop:
  %i = phi i64 [ 0, %entry ], [ %i.next, %loop ]
  %a.ptr = getelementptr float, ptr %a, i64 %i
  %b.ptr = getelementptr float, ptr %b, i64 %i
  %c.ptr = getelementptr float, ptr %c, i64 %i
  %a.val = load float, ptr %a.ptr
  %b.val = load float, ptr %b.ptr
  %sum = fadd float %a.val, %b.val
  store float %sum, ptr %c.ptr
  %i.next = add i64 %i, 1
  %cmp = icmp slt i64 %i.next, %n
  br i1 %cmp, label %loop, label %exit

; 向量化后(4 路 SIMD)
vector.body:
  %index = phi i64 [ 0, %entry ], [ %index.next, %vector.body ]
  %a.ptr = getelementptr float, ptr %a, i64 %index
  %b.ptr = getelementptr float, ptr %b, i64 %index
  %c.ptr = getelementptr float, ptr %c, i64 %index
  %a.vec = load <4 x float>, ptr %a.ptr
  %b.vec = load <4 x float>, ptr %b.ptr
  %sum.vec = fadd <4 x float> %a.vec, %b.vec
  store <4 x float> %sum.vec, ptr %c.ptr
  %index.next = add i64 %index, 4
  %cmp = icmp slt i64 %index.next, %n
  br i1 %cmp, label %vector.body, label %exit

8.4.3 向量化控制

// Clang pragma 控制
#pragma clang loop vectorize(enable)     // 启用向量化
#pragma clang loop vectorize_width(8)    // 指定向量宽度
#pragma clang loop interleave(enable)    // 启用交错
#pragma clang loop interleave_count(4)   // 交错因子

// OpenMP SIMD
#pragma omp simd
for (int i = 0; i < n; i++) {
    c[i] = a[i] + b[i];
}

// GCC/Clang 属性
__attribute__((vector_size(16))) typedef float v4f;
# 控制向量化
clang -O2 -fvectorize file.c                    # 启用向量化
clang -O2 -fno-vectorize file.c                 # 禁用向量化
clang -O2 -mllvm -force-vector-width=8 file.c   # 强制向量宽度
clang -O2 -Rpass=loop-vectorize file.c          # 报告向量化
clang -O2 -Rpass-missed=loop-vectorize file.c   # 报告未向量化
clang -O2 -Rpass-analysis=loop-vectorize file.c # 详细分析

8.4.4 SLP 向量化

// SLP 向量化:将连续独立运算打包
void process(float *out, float a0, float a1, float b0, float b1) {
    // 这些独立运算可以打包为 SIMD
    out[0] = a0 + b0;
    out[1] = a1 + b1;
    out[2] = a0 * b0;
    out[3] = a1 * b1;
}

8.5 循环优化

8.5.1 循环不变量外提 (LICM)

; 优化前 — 循环内有不变量计算
loop:
  %i = phi i64 [ 0, %entry ], [ %i.next, %loop ]
  %invariant = load i32, ptr @global    ; 每次循环都加载!
  %val = add i32 %i, %invariant
  %i.next = add i64 %i, 1
  %cmp = icmp slt i64 %i.next, 100
  br i1 %cmp, label %loop, label %exit

; 优化后 — 不变量外提到循环外
preheader:
  %invariant = load i32, ptr @global
  br label %loop

loop:
  %i = phi i64 [ 0, %preheader ], [ %i.next, %loop ]
  %val = add i32 %i, %invariant
  %i.next = add i64 %i, 1
  %cmp = icmp slt i64 %i.next, 100
  br i1 %cmp, label %loop, label %exit

8.5.2 循环展开 (Loop Unroll)

; 优化前
loop:
  %i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
  %sum = phi i32 [ 0, %entry ], [ %sum.next, %loop ]
  call void @process(i32 %i)
  %sum.next = add i32 %sum, %i
  %i.next = add i32 %i, 1
  %cmp = icmp slt i32 %i.next, 100
  br i1 %cmp, label %loop, label %exit

; 优化后(展开因子 4)
loop:
  %i = phi i32 [ 0, %entry ], [ %i.next, %loop ]
  %sum = phi i32 [ 0, %entry ], [ %sum.next, %loop ]
  call void @process(i32 %i)
  %sum.1 = add i32 %sum, %i
  %i.1 = add i32 %i, 1
  call void @process(i32 %i.1)
  %sum.2 = add i32 %sum.1, %i.1
  %i.2 = add i32 %i.1, 1
  call void @process(i32 %i.2)
  %sum.3 = add i32 %sum.2, %i.2
  %i.3 = add i32 %i.2, 1
  call void @process(i32 %i.3)
  %sum.next = add i32 %sum.3, %i.3
  %i.next = add i32 %i.3, 1
  %cmp = icmp slt i32 %i.next, 100
  br i1 %cmp, label %loop, label %exit
# 控制循环展开
clang -O2 -mllvm -unroll-count=4 file.c       # 指定展开因子
clang -O2 -mllvm -unroll-threshold=200 file.c  # 调整展开阈值
clang -O2 -fno-unroll-loops file.c             # 禁用展开

8.5.3 循环交换 (Loop Interchange)

// 优化前 — cache 不友好
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        A[i][j] = B[i][j] + 1;

// 优化后 — 循环交换,cache 友好
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        A[i][j] = B[i][j] + 1;
# 启用循环交换(O3 默认)
clang -O3 -mllvm -enable-loopinterchange file.c

8.5.4 归纳变量简化 (IndVar Simplify)

; 优化前 — 复杂的归纳变量
loop:
  %i = phi i64 [ 0, %entry ], [ %i.next, %loop ]
  %j = mul i64 %i, 4                    ; j = i * 4
  %ptr = getelementptr i32, ptr %arr, i64 %j
  store i32 0, ptr %ptr
  %i.next = add i64 %i, 1
  %cmp = icmp slt i64 %i.next, 100
  br i1 %cmp, label %loop, label %exit

; 优化后 — 归纳变量简化
loop:
  %ptr = phi ptr [ %arr, %entry ], [ %ptr.next, %loop ]
  store i32 0, ptr %ptr
  %ptr.next = getelementptr i32, ptr %ptr, i64 1
  %cmp = icmp ne ptr %ptr.next, %end.ptr
  br i1 %cmp, label %loop, label %exit

8.5.5 循环优化总结

优化 Pass 名称 作用
LICM licm 循环不变量外提
循环展开 loop-unroll 减少循环开销
循环交换 loop-interchange 改善缓存局部性
归纳变量简化 indvars 简化归纳变量
循环旋转 loop-rotate do-while 形式
循环删除 loop-deletion 消除空循环
循环分发 loop-distribute 拆分循环以向量化
循环融合 loop-fuse 合并循环
循环展平 loop-flatten 嵌套循环展平
循环强度削减 loop-reduce 降低循环内运算强度

8.6 死代码消除

8.6.1 多层次死代码消除

// 层次一:DCE — 简单死代码消除
// 消除无副作用的未使用指令
int x = compute();  // 如果 x 未使用,可以消除

// 层次二:ADCE — 激进死代码消除
// 沿着控制流追踪,消除不被使用的分支
int y = 42;
if (y > 0) {  // 如果 y 是常量,整个 if 可以简化
    ...
}

// 层次三:GlobalDCE — 全局死代码消除
// 消除模块中未使用的全局变量和函数
static void unused_function() { ... }  // 可以删除

// 层次四:DSE — 死存储消除
// 消除被覆盖的存储
*p = 1;   // 这个存储是死的
*p = 2;   // 只有这个有效

8.7 过程间优化与 LTO

LTO 在链接时跨编译单元进行优化,可以实现跨文件内联:

# 传统 LTO(Full LTO)
clang -flto -O2 -c a.c -o a.o
clang -flto -O2 -c b.c -o b.o
clang -flto -O2 a.o b.o -o program

# Thin LTO(推荐,编译更快)
clang -flto=thin -O2 -c a.c -o a.o
clang -flto=thin -O2 -c b.c -o b.o
clang -flto=thin -O2 a.o b.o -o program

# 查看 LTO 优化
clang -flto=thin -O2 -Wl,--save-temps a.o b.o -o program

8.7.2 Full LTO vs Thin LTO

特性 Full LTO Thin LTO
链接速度 🐢 慢 ⚡ 快
内存使用 📦📦📦 高 📦 低
优化质量 🚀🚀 略好 🚀 好
增量编译 ❌ 不支持 ✅ 支持
并行优化 ❌ 有限 ✅ 充分并行
推荐场景 极致优化 一般场景

8.7.3 LTO 的优化能力

无 LTO:
  a.c → a.o (独立优化)
  b.c → b.o (独立优化)
  a.o + b.o → program (仅链接)

有 LTO:
  a.c → a.bc
  b.c → b.bc
  a.bc + b.bc → 合并 LLVM IR → 全局优化 → program

LTO 可以做到:
✅ 跨文件内联
✅ 跨文件常量传播
✅ 跨文件死代码消除
✅ 跨文件别名分析
✅ 跨文件虚函数去虚拟化

8.8 Profile-Guided Optimization (PGO)

8.8.1 PGO 流程

# 步骤 1: 插桩编译
clang -fprofile-generate -O2 -o program_instrumented program.c

# 步骤 2: 运行收集 profile
./program_instrumented < typical_input.txt
# 生成 default.profraw

# 步骤 3: 合并 profile
llvm-profdata merge -output=program.profdata default.profraw

# 步骤 4: 使用 profile 优化编译
clang -fprofile-use=program.profdata -O2 -o program_optimized program.c

# 验证
./program_optimized

8.8.2 PGO 的优化效果

优化 无 PGO 有 PGO
内联决策 基于启发式 基于实际调用频率
分支预测 无信息 知道哪个分支更可能
基本块布局 默认顺序 热路径在前
循环展开 默认因子 根据执行次数调整
函数排序 默认 热函数聚集

8.8.3 BOLT 二进制优化

# BOLT — 基于 profile 的二进制后链接优化
# 需要链接时使用 --emit-relocs

# 编译时启用 relocations
clang -O2 -Wl,--emit-relocs -o program program.c

# 收集 profile (使用 perf)
perf record -e cycles:u -j any,u -o perf.data -- ./program
perf2bolt -p perf.data -o perf.fdata ./program

# BOLT 优化
llvm-bolt ./program -o program.bolt -data=perf.fdata

8.9 优化调试

# 查看优化前后的 IR
opt -O2 -S input.ll -o output.ll

# 查看特定优化的效果
opt -passes='instcombine' -S input.ll -o output.ll

# 查看优化报告
opt -passes='loop-vectorize' -pass-remarks='loop-vectorize' input.ll -o /dev/null 2>&1

# Clang 优化报告
clang -O2 -Rpass='.*' file.c 2>&1          # 成功的优化
clang -O2 -Rpass-missed='.*' file.c 2>&1    # 未应用的优化
clang -O2 -Rpass-analysis='.*' file.c 2>&1  # 详细分析

# 查看优化统计
opt -O2 -stats input.ll -o /dev/null 2>&1 | sort -t= -k2 -rn | head -20

8.10 本章小结

优化类别 主要技术 效果
内联 Inliner Pass 消除调用开销,暴露跨函数优化
常量传播 SCCP, ConstantPropagation 编译期计算常量
值编号 GVN 消除冗余计算
向量化 LoopVectorize, SLPVectorize SIMD 并行
循环优化 LICM, Unroll, Interchange 循环效率提升
死代码消除 DCE, ADCE, DSE, GlobalDCE 减少无效代码
LTO Full LTO, Thin LTO 跨文件优化
PGO Profile-Guided Optimization 基于实际运行数据优化

扩展阅读

  1. LLVM Optimization Remarks — 优化报告
  2. Auto-Vectorization in LLVM — 向量化文档
  3. Link Time Optimization — LTO 文档
  4. Profile Guided Optimization — PGO 文档
  5. BOLT — 二进制优化器

下一章: 第 9 章:代码生成 — 学习 LLVM 如何将 IR 转换为目标机器码。