mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1752 字
5 分钟
循环优化:展开、向量化与不变量外提
2026-01-10

程序 80% 的时间花在 20% 的代码上,而这 20% 的代码几乎总是循环。循环优化是编译器优化的”皇冠上的明珠”——一个成功的循环优化可以将程序性能提升数倍甚至数十倍。

这一章的主角是循环优化——循环展开如何减少分支开销?向量化如何利用 SIMD 指令?循环不变量外提如何避免重复计算?强度消减如何将昂贵的运算替换为廉价的运算?

一、循环优化的重要性#

1.1 为什么循环是优化的重点?#

graph TB CODE["程序代码"] --> HOT["热点代码 20%"] CODE --> COLD["冷代码 80%"] HOT --> LOOP["循环体"] HOT --> INNER["内层循环"] TIME["运行时间"] --> HOT_TIME["80% 在热点"] TIME --> COLD_TIME["20% 在冷代码"] style HOT fill:#fce4ec,stroke:#c62828 style LOOP fill:#e8f5e9,stroke:#2e7d32 style HOT_TIME fill:#fce4ec,stroke:#c62828
优化类型典型加速比适用频率
循环展开1.2x-2x
向量化2x-8x
循环不变量外提1.1x-3x
强度消减1.1x-2x
归纳变量简化1.1x-1.5x

1.2 循环的分析框架#

编译器需要识别循环结构才能进行优化:

class LoopInfo:
"""循环信息"""
def __init__(self, header, body, exits, parent=None):
self.header = header # 循环头基本块
self.body = body # 循环体基本块集合
self.exits = exits # 退出基本块集合
self.parent = parent # 外层循环
self.depth = 1 # 嵌套深度
if parent:
self.depth = parent.depth + 1
def is_innermost(self):
"""是否是最内层循环"""
return len(self.sub_loops) == 0
def identify_loops(cfg):
"""识别 CFG 中的自然循环"""
loops = []
# 查找回边(header 支配 latch 的边)
for block in cfg.blocks:
for succ in block.successors:
if dominates(succ, block, cfg):
# 找到回边 block → succ
loop_body = find_loop_body(succ, block, cfg)
loop = LoopInfo(header=succ, body=loop_body, exits=[])
loops.append(loop)
return loops

二、循环展开(Loop Unrolling)#

2.1 基本原理#

循环展开通过复制循环体减少循环控制开销(分支判断、计数器更新):

// 优化前:每次迭代都有分支和计数器更新
for (int i = 0; i < 100; i++) {
a[i] = b[i] + c[i];
}
// 完全展开(100 次迭代全部展开)
a[0] = b[0] + c[0];
a[1] = b[1] + c[1];
// ... 98 行省略 ...
a[99] = b[99] + c[99];
// 部分展开(4 倍展开)
for (int i = 0; i < 100; i += 4) {
a[i] = b[i] + c[i];
a[i+1] = b[i+1] + c[i+1];
a[i+2] = b[i+2] + c[i+2];
a[i+3] = b[i+3] + c[i+3];
}

2.2 展开的收益与代价#

flowchart LR subgraph 收益 R1["减少分支开销"] R2["增加指令级并行"] R3["暴露更多优化机会"] end subgraph 代价 C1["代码体积增大"] C2["指令缓存压力"] C3["寄存器压力增大"] end R1 --> NET["净收益取决于<br/>循环体大小和迭代次数"] C1 --> NET style 收益 fill:#e8f5e9,stroke:#2e7d32 style 代价 fill:#fce4ec,stroke:#c62828
展开因子分支减少代码增大寄存器压力推荐场景
2x50%2x通用
4x75%4x计算密集
8x87.5%8xSIMD 前置
完全100%N*x极高小循环(<10次)

2.3 展开的实现#

class LoopUnroller:
"""循环展开器"""
def unroll(self, loop, factor):
"""将循环展开 factor 倍"""
if factor <= 1:
return loop
# 检查迭代次数是否已知
trip_count = self._get_trip_count(loop)
if trip_count is None:
# 迭代次数未知,需要生成余数循环
return self._unroll_unknown_count(loop, factor)
if trip_count % factor != 0:
# 迭代次数不是 factor 的倍数,需要处理余数
return self._unroll_with_remainder(loop, factor, trip_count)
# 完美展开
return self._unroll_perfect(loop, factor)
def _unroll_perfect(self, loop, factor):
"""完美展开(迭代次数是 factor 的倍数)"""
new_body = []
for i in range(factor):
# 复制循环体,替换归纳变量
for inst in loop.body_instructions:
new_inst = self._rename_inst(inst, i)
new_body.append(new_inst)
# 更新循环步长
loop.step *= factor
loop.body_instructions = new_body
return loop
Warning

循环展开不是越多越好。过大的展开因子会导致指令缓存未命中增加,反而降低性能。LLVM 的 -funroll-loops 选项会根据循环体大小和目标处理器的缓存大小自动选择展开因子。

三、向量化(Vectorization)#

3.1 SIMD 指令基础#

SIMD(Single Instruction Multiple Data)一条指令同时处理多个数据:

flowchart LR subgraph 标量["标量处理"] S1["a[0]+b[0]"] --> S2["a[1]+b[1]"] --> S3["a[2]+b[2]"] --> S4["a[3]+b[3]"] end subgraph SIMD["SIMD 处理"] V1["a[0:4]+b[0:4]<br/>一条指令同时处理4个"] end style 标量 fill:#fff3e0,stroke:#e65100 style SIMD fill:#e8f5e9,stroke:#2e7d32
SIMD 扩展位宽32位元素数平台
SSE21284x86
AVX2568x86
AVX-51251216x86
NEON1284ARM
SVE128-2048可变ARM

3.2 循环向量化(Loop Vectorization)#

循环向量化将标量循环转换为 SIMD 循环:

// 标量循环
for (int i = 0; i < 1024; i++) {
c[i] = a[i] + b[i];
}
// 向量化后(AVX2,8 个 float 同时处理)
for (int i = 0; i < 1024; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]); // 加载 8 个 float
__m256 vb = _mm256_loadu_ps(&b[i]); // 加载 8 个 float
__m256 vc = _mm256_add_ps(va, vb); // 一次加 8 个
_mm256_storeu_ps(&c[i], vc); // 存储 8 个 float
}

3.3 向量化的条件#

不是所有循环都可以向量化,编译器需要检查以下条件:

条件说明示例
无循环携带依赖迭代之间无数据依赖a[i] = a[i-1] + 1 不可向量化
连续内存访问数组元素在内存中连续a[i*stride] stride=1 最佳
循环次数已知至少在入口可计算while(cond) 难以向量化
无函数调用或调用是 SIMD 安全的printf 不可向量化
无分支或分支可转换为 mask简单 if 可用 mask 向量化

3.4 依赖分析#

class DependencyAnalyzer:
"""循环依赖分析器"""
def analyze(self, loop):
"""分析循环中的依赖关系"""
deps = []
for i, inst1 in enumerate(loop.body):
for j, inst2 in enumerate(loop.body):
if i >= j:
continue
# 检查是否有依赖
dep = self._check_dependency(inst1, inst2)
if dep:
deps.append(dep)
return deps
def _check_dependency(self, inst1, inst2):
"""检查两条指令之间的依赖"""
# 写后读(WAR)— 反依赖
if inst1.is_read and inst2.is_write:
if self._same_array(inst1, inst2):
return Dependency('WAR', inst1, inst2)
# 读后写(RAW)— 真依赖
if inst1.is_write and inst2.is_read:
if self._same_array(inst1, inst2):
return Dependency('RAW', inst1, inst2)
# 写后写(WAW)— 输出依赖
if inst1.is_write and inst2.is_write:
if self._same_array(inst1, inst2):
return Dependency('WAW', inst1, inst2)
return None

3.5 SLP 向量化#

SLP(Superword-Level Parallelism)向量化不同于循环向量化——它在同一迭代内寻找可以打包为 SIMD 的操作:

// SLP 向量化示例
struct Point { float x, y, z, w; };
void transform(Point* p, float s) {
p->x = p->x * s; // 这四个乘法
p->y = p->y * s; // 可以打包为
p->z = p->z * s; // 一条 SIMD 乘法
p->w = p->w * s; // 指令
}
向量化类型目标适用场景
循环向量化跨迭代并行大数组遍历
SLP 向量化迭代内并行结构体操作、解相关计算

四、循环不变量外提(LICM)#

4.1 基本原理#

循环不变量是在循环中每次迭代结果都相同的计算——可以提到循环外面:

// 优化前
for (int i = 0; i < n; i++) {
a[i] = b[i] * (x + y); // x + y 是循环不变量
}
// 优化后
int temp = x + y; // 外提到循环前
for (int i = 0; i < n; i++) {
a[i] = b[i] * temp;
}

4.2 LICM 的实现#

class LICM:
"""循环不变量外提"""
def hoist(self, loop):
"""将循环不变量外提到循环前置节点"""
preheader = self._ensure_preheader(loop)
changed = True
while changed:
changed = False
for block in loop.body:
new_insts = []
for inst in block.instructions:
if self._is_loop_invariant(inst, loop):
# 外提到 preheader
preheader.instructions.append(inst)
changed = True
else:
new_insts.append(inst)
block.instructions = new_insts
return loop
def _is_loop_invariant(self, inst, loop):
"""判断指令是否是循环不变量"""
# 有副作用不是不变量
if inst.op in ('store', 'call', 'load'):
return False
# 终止指令不是不变量
if inst.op in ('br', 'ret', 'phi'):
return False
# 所有操作数都是循环不变量
for operand in inst.operands:
if isinstance(operand, str):
# 检查操作数是否在循环外定义
def_block = self._get_definition_block(operand)
if def_block in loop.body:
return False
return True
def _ensure_preheader(self, loop):
"""确保循环有前置节点"""
if loop.preheader:
return loop.preheader
# 创建前置节点
preheader = BasicBlock(f"{loop.header.label}.preheader")
# 调整 CFG 边
# ...
return preheader

4.3 LICM 的安全条件#

外提必须保证不改变程序语义

条件说明示例
定义支配使用外提后的定义必须支配所有使用循环内定义的变量
无副作用load 可能有异常,不能随意外提*ptr 可能段错误
不改变异常行为外提不能改变异常抛出的时机除零异常
循环可能不执行需要判断循环是否至少执行一次for(i=0;i<n;i++) n 可能是 0
Note

对于 load 指令的外提,编译器需要证明:1) 地址在循环中不变;2) 没有其他 store 指令可能修改该地址的值(别名分析);3) 循环至少执行一次。如果无法证明,编译器会保守地不外提。

五、强度消减(Strength Reduction)#

5.1 基本原理#

昂贵的运算替换为廉价的运算

替换代价变化示例
乘法 → 加法O(1) → O(1) 但更快i * 4j += 4
乘法 → 移位乘法器 → 移位器i * 8i << 3
除法 → 移位极慢 → 极快i / 4i >> 2
取模 → 位与极慢 → 极快i % 8i & 7
乘方 → 乘法循环 → 单次x * x 代替 pow(x, 2)

5.2 归纳变量的强度消减#

// 优化前:数组索引使用乘法
for (int i = 0; i < n; i++) {
a[i * 4] = 0; // 每次迭代计算 i * 4
}
// 强度消减后:用加法替代乘法
for (int i = 0, j = 0; i < n; i++, j += 4) {
a[j] = 0; // j 每次加 4,无需乘法
}

5.3 强度消减的实现#

class StrengthReducer:
"""强度消减器"""
def reduce(self, loop):
"""对循环中的归纳变量进行强度消减"""
# 识别归纳变量
ivs = self._find_induction_vars(loop)
for iv in ivs:
# 查找 iv * constant 模式
for user in iv.users:
if user.op == 'mul' and user.operands[1].is_constant:
# 替换为新的归纳变量
new_iv = self._create_derived_iv(
iv, user.operands[1].value, loop
)
user.replace_with(new_iv)
return loop
def _find_induction_vars(self, loop):
"""识别归纳变量"""
ivs = []
for block in loop.body:
for inst in block.instructions:
if inst.op == 'phi':
# phi 节点可能是归纳变量
# 检查递增模式:iv = iv + step
if self._is_linear_induction(inst, loop):
ivs.append(inst)
return ivs

六、归纳变量简化(IndVar Simplification)#

6.1 基本原理#

归纳变量简化将不同的归纳变量统一为单一的标准归纳变量:

// 优化前:三个归纳变量
for (int i = 0; i < n; i++) {
int j = i * 4; // 派生归纳变量
int k = i * 4 + 1; // 派生归纳变量
a[j] = 0;
b[k] = 1;
}
// 归纳变量简化后
for (int i = 0; i < n * 4; i += 4) {
a[i] = 0;
b[i + 1] = 1;
}

6.2 循环优化的协同#

flowchart TB LICM["循环不变量外提<br/>LICM"] --> INDVAR["归纳变量简化"] INDVAR --> STRENGTH["强度消减"] STRENGTH --> UNROLL["循环展开"] UNROLL --> VEC["向量化"] style LICM fill:#e3f2fd,stroke:#1565c0 style INDVAR fill:#e0f2f1,stroke:#00695c style STRENGTH fill:#fff3e0,stroke:#e65100 style UNROLL fill:#e8f5e9,stroke:#2e7d32 style VEC fill:#fce4ec,stroke:#c62828

优化的典型顺序:先外提不变量(LICM),再简化归纳变量,然后强度消减,展开循环,最后向量化。

七、循环优化的实际效果#

7.1 一个完整的优化示例#

// 原始代码
void add_arrays(float* a, float* b, float* c, int n, float scale) {
for (int i = 0; i < n; i++) {
c[i] = (a[i] + b[i]) * scale;
}
}
# 查看不同优化级别的汇编输出
clang -O0 -S add_arrays.c # 无优化
clang -O1 -S add_arrays.c # 基本优化
clang -O2 -S add_arrays.c # 标准优化(包含向量化)
clang -O3 -S add_arrays.c # 激进优化(更多展开)

7.2 优化级别对循环的影响#

优化级别LICM强度消减展开向量化典型加速
-O01x
-O11.5-2x
-O23-8x
-O3激进3-10x

7.3 阻止向量化的常见原因#

// 1. 循环携带依赖
for (int i = 1; i < n; i++) {
a[i] = a[i-1] + 1; // 依赖前一次迭代
}
// 2. 未知指针别名
void add(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i]; // 如果 c 和 a/b 重叠呢?
}
}
// 3. 循环内分支
for (int i = 0; i < n; i++) {
if (a[i] > 0) { // 分支难以向量化
b[i] = a[i];
}
}
// 解决方案
void add_restrict(float* __restrict__ a,
float* __restrict__ b,
float* __restrict__ c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i]; // restrict 保证不重叠
}
}
Tip

__restrict__ 关键字告诉编译器指针不会重叠,这是帮助编译器向量化的最有效方法之一。在 C99 中使用 restrict,在 C++ 中使用 __restrict__(编译器扩展)。

八、动手实践#

8.1 实验一:观察循环向量化#

cat > vec_test.c << 'EOF'
void add(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
EOF
# 查看向量化报告
clang -O2 -Rpass=loop-vectorize vec_test.c -c 2>&1
# 输出:vec_test.c:3:5: remark: vectorized loop (vectorization width: 8, interleaved count: 4)
# 查看汇编中的 SIMD 指令
clang -O2 -S vec_test.c -o vec_test.s
grep -E "(vmov|vadd|vld|vst)" vec_test.s

8.2 实验二:对比 restrict 的影响#

cat > restrict_test.c << 'EOF'
// 无 restrict
void add1(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++) c[i] = a[i] + b[i];
}
// 有 restrict
void add2(float* __restrict__ a, float* __restrict__ b,
float* __restrict__ c, int n) {
for (int i = 0; i < n; i++) c[i] = a[i] + b[i];
}
EOF
clang -O2 -S restrict_test.c -o restrict_test.s
diff <(grep -A5 "add1:" restrict_test.s) <(grep -A5 "add2:" restrict_test.s)

8.3 实验三:手动向量化 vs 自动向量化#

#include <immintrin.h>
// 自动向量化
void auto_vec(float* a, float* b, float* c, int n) {
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
}
// 手动向量化
void manual_vec(float* a, float* b, float* c, int n) {
int i = 0;
for (; i + 7 < n; i += 8) {
__m256 va = _mm256_loadu_ps(&a[i]);
__m256 vb = _mm256_loadu_ps(&b[i]);
__m256 vc = _mm256_add_ps(va, vb);
_mm256_storeu_ps(&c[i], vc);
}
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}

九、本章小结#

上一章中,常量折叠、死代码消除和公共子表达式消除等基础优化展示了”不改变语义的前提下让程序更快”的思路。但这些优化对循环的效果有限——而程序 80% 的时间恰恰花在循环中。要真正释放性能,编译器必须深入循环结构做变换:展开、向量化、不变量外提。

概念要点
循环展开复制循环体减少分支开销,注意代码体积和寄存器压力
向量化SIMD 指令同时处理多个数据,需要无循环携带依赖
LICM循环不变量外提到循环前置节点,注意副作用和安全条件
强度消减乘法→加法/移位,除法→移位,取模→位与
归纳变量简化统一不同归纳变量为标准形式
依赖分析RAW/WAR/WAW 三种依赖,RAW 是真依赖
SLP 向量化迭代内的 SIMD 并行
restrict告诉编译器指针不重叠,帮助向量化

用了整章的篇幅拆解循环优化。下一章进入 寄存器分配,看看编译器如何在有限的物理寄存器中安排无限多的虚拟寄存器。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

循环优化:展开、向量化与不变量外提
https://blog.souloss.com/posts/compiler/loop-optimization/
作者
Souloss
发布于
2026-01-10
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时