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

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
优化技术O1O2O3OsOz
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 名称作用
LICMlicm循环不变量外提
循环展开loop-unroll减少循环开销
循环交换loop-interchange改善缓存局部性
归纳变量简化indvars简化归纳变量
循环旋转loop-rotatedo-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 LTOThin 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, SLPVectorizeSIMD 并行
循环优化LICM, Unroll, Interchange循环效率提升
死代码消除DCE, ADCE, DSE, GlobalDCE减少无效代码
LTOFull LTO, Thin LTO跨文件优化
PGOProfile-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 转换为目标机器码。