GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

软件研发 --- 应知应会 之 编译器优化

 

一、概述

编译器优化是指编译器在将源代码转换为机器代码的过程中,自动改进代码的效率,使生成的程序运行更快、占用内存更少,同时保持程序语义不变。

text
源代码 → [前端分析] → IR中间表示 → [优化Pass] → [后端] → 机器码
                                   优化发生在这里

二、优化的层次

text
┌─────────────────────────────────────┐
│         高层优化(源码级别)          │  算法级别
├─────────────────────────────────────┤
│         中间表示优化(IR级别)        │  编译器主要工作
├─────────────────────────────────────┤
│         低层优化(机器码级别)        │  指令选择/寄存器分配
└─────────────────────────────────────┘

三、常见优化技术详解

3.1 常量折叠(Constant Folding)

在编译期计算常量表达式,避免运行时计算。

C
// 优化前
int x = 2 * 3 + 4;
int y = sizeof(int) * 8;

// 优化后(编译器直接替换)
int x = 10;
int y = 32;
C
// 更复杂的例子
if (1 == 1) {        // 编译器知道这永远为真
    do_something();
}
// 优化后直接变成:
do_something();

3.2 常量传播(Constant Propagation)

追踪变量的常量值,并将变量替换为常量。

C
// 优化前
int a = 5;
int b = a + 3;    // a是常量5
int c = b * 2;    // b是常量8

// 优化后
int a = 5;
int b = 8;        // 5 + 3 = 8
int c = 16;       // 8 * 2 = 16

3.3 死代码消除(Dead Code Elimination)

删除永远不会执行或结果不会被使用的代码。

C
// 优化前
int foo() {
    int x = compute();   // x被计算但从未使用
    int y = 42;
    return y;
    int z = 100;         // return后的代码不可达
}

// 优化后
int foo() {
    return 42;
}
C
// 条件死代码
#define DEBUG 0
if (DEBUG) {
    printf("debug info");   // 永远不执行,直接删除
}

3.4 公共子表达式消除(CSE - Common Subexpression Elimination)

避免重复计算相同的表达式。

C
// 优化前
float a = b*c + g;
float d = b*c * e;    // b*c 被计算了两次

// 优化后
float tmp = b*c;      // 只计算一次
float a = tmp + g;
float d = tmp * e;
C
// 数组访问中的CSE
a[i*width + j] = a[i*width + j] + 1;

// 优化后
int idx = i*width + j;   // 索引只计算一次
a[idx] = a[idx] + 1;

3.5 循环优化

3.5.1 循环不变量外提(Loop Invariant Code Motion)

将循环体内不随迭代改变的计算移到循环外。

C
// 优化前
for (int i = 0; i < n; i++) {
    a[i] = b[i] * sqrt(x);   // sqrt(x) 每次循环都计算
}

// 优化后
double tmp = sqrt(x);          // 移到循环外,只计算一次
for (int i = 0; i < n; i++) {
    a[i] = b[i] * tmp;
}

3.5.2 循环展开(Loop Unrolling)

减少循环控制开销,增加指令级并行。

C
// 优化前
for (int i = 0; i < 100; i++) {
    a[i] = b[i] + c[i];
}

// 展开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];
}
text
展开前:每次迭代 = 计算 + 增量i + 比较 + 跳转
展开后:每4次计算只需要 1次增量 + 1次比较 + 1次跳转

3.5.3 循环融合(Loop Fusion)

将多个循环合并,提高缓存利用率。

C
// 优化前:两次遍历数组
for (int i = 0; i < n; i++) a[i] = b[i] + 1;
for (int i = 0; i < n; i++) c[i] = a[i] * 2;

// 优化后:一次遍历
for (int i = 0; i < n; i++) {
    a[i] = b[i] + 1;
    c[i] = a[i] * 2;   // 更好的缓存局部性
}

3.5.4 循环分裂(Loop Fission/Distribution)

将一个循环拆分,提高向量化可能性。

C
// 优化前(难以向量化,因为有依赖)
for (int i = 0; i < n; i++) {
    a[i] = a[i-1] + b[i];   // 存在依赖
    c[i] = d[i] * e[i];     // 无依赖,可向量化
}

// 优化后
for (int i = 0; i < n; i++)
    a[i] = a[i-1] + b[i];   // 保持顺序

for (int i = 0; i < n; i++)
    c[i] = d[i] * e[i];     // 可以SIMD向量化

3.5.5 循环交换(Loop Interchange)

改变嵌套循环顺序,提高内存访问局部性。

C
// 优化前(按列访问,缓存不友好)
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += A[i][j];   // 跨行访问

// 优化后(按行访问,缓存友好)
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += A[i][j];   // 连续内存访问
text
内存布局(行主序):
A[0][0] A[0][1] A[0][2] | A[1][0] A[1][1] ...
←── 缓存行 ──→

按行访问:顺序读取,缓存命中率高 ✓
按列访问:跳跃读取,频繁缓存缺失 ✗

3.6 内联展开(Inlining)

将函数调用替换为函数体,消除调用开销。

C
// 优化前
inline int add(int a, int b) {
    return a + b;
}
int result = add(x, y);

// 内联后(编译器直接替换)
int result = x + y;   // 无函数调用开销

函数调用的开销:

text
调用函数需要:
1. 保存寄存器
2. 设置参数(压栈或寄存器传递)
3. 跳转到函数地址
4. 建立栈帧
5. 执行函数体
6. 恢复现场
7. 返回

内联后:只有函数体执行

3.7 强度削减(Strength Reduction)

用代价更低的操作替换代价高的操作。

C
// 乘法 → 加法
// 优化前
for (int i = 0; i < n; i++) {
    a[i*8] = ...;    // 每次乘法
}

// 优化后
for (int i = 0, j = 0; i < n; i++, j += 8) {
    a[j] = ...;      // 改为加法,代价更低
}
C
// 乘以2的幂 → 移位
x * 2   →   x << 1
x * 4   →   x << 2
x / 8   →   x >> 3
x % 16  →   x & 15
C
// 除法 → 乘以倒数(浮点)
x / 3.0  →  x * 0.333...   // 乘法比除法快

3.8 尾递归优化(Tail Call Optimization)

将尾递归转换为循环,避免栈溢出。

C
// 优化前(尾递归)
int factorial(int n, int acc) {
    if (n == 0) return acc;
    return factorial(n-1, n*acc);  // 尾调用
}

// 优化后(编译器转为循环)
int factorial(int n, int acc) {
    while (n != 0) {
        acc = n * acc;
        n = n - 1;
    }
    return acc;
}

3.9 向量化(Vectorization / SIMD)

利用SIMD指令同时处理多个数据。

C
// 标量代码
for (int i = 0; i < 8; i++) {
    c[i] = a[i] + b[i];
}

// 向量化后(概念示意)
// 一条SIMD指令同时处理8个float
__m256 va = _mm256_load_ps(a);   // 加载8个float
__m256 vb = _mm256_load_ps(b);   // 加载8个float  
__m256 vc = _mm256_add_ps(va, vb); // 8个加法同时执行
_mm256_store_ps(c, vc);
text
标量:  a[0]+b[0], a[1]+b[1], ..., a[7]+b[7]  → 8次操作
SIMD:  [a0,a1,a2,a3,a4,a5,a6,a7]
      + [b0,b1,b2,b3,b4,b5,b6,b7]
      = [c0,c1,c2,c3,c4,c5,c6,c7]            → 1次操作

3.10 别名分析(Alias Analysis)

确定指针是否指向相同内存,从而解锁更多优化。

C
// 编译器不确定a和b是否重叠,难以优化
void add(float* a, float* b, float* c, int n) {
    for (int i = 0; i < n; i++)
        c[i] = a[i] + b[i];
}

// 使用restrict告诉编译器指针不重叠
void add(float* restrict a, 
         float* restrict b, 
         float* restrict c, int n) {
    for (int i = 0; i < n; i++)
        c[i] = a[i] + b[i];   // 现在可以安全向量化
}

四、GCC/Clang 优化等级

text
优化等级    说明                          典型用途
─────────────────────────────────────────────────
-O0        无优化(默认)                  调试
-O1        基本优化,不增加编译时间        
-O2        推荐优化,不做空间换时间        生产环境 ✓
-O3        激进优化(向量化/内联等)        高性能计算
-Os        优化代码大小                    嵌入式系统
-Oz        最小化代码大小(Clang)         
-Og        调试友好的优化                  开发调试
Bash
# 查看优化后的汇编
gcc -O2 -S -o output.s input.c

# 查看优化报告
gcc -O2 -fopt-info-vec program.c    # 向量化信息
gcc -O2 -fopt-info-all program.c    # 所有优化信息

五、优化的局限性

5.1 可观察副作用限制优化

C
// 编译器不能消除这个"死代码"
// 因为volatile表示可观察副作用
volatile int* hw_reg = (volatile int*)0x1000;
*hw_reg = 0;    // 不能被优化掉!

5.2 指针别名阻止优化

C
void copy(int* dst, int* src, int n) {
    for (int i = 0; i < n; i++)
        dst[i] = src[i];
    // 如果dst和src重叠,向量化可能产生错误结果
    // 编译器必须保守处理
}

5.3 函数调用阻止优化

C
// 编译器不知道外部函数的副作用
for (int i = 0; i < n; i++) {
    result += external_function(i);  // 无法优化循环
}

六、优化可视化工具

Bash
# 1. Compiler Explorer(godbolt.org)
#    在线查看不同优化等级的汇编输出

# 2. 查看优化passes(LLVM)
clang -O2 -mllvm -print-after-all program.c

# 3. 生成优化报告
g++ -O2 -fopt-info program.cpp

# 4. 查看向量化
g++ -O3 -fopt-info-vec-optimized program.cpp

七、总结

text
优化类型          收益来源              适用场景
────────────────────────────────────────────────
常量折叠/传播     减少运行时计算         常量表达式
死代码消除        减少代码量             不可达代码
内联展开          消除函数调用开销       小函数、热点
循环优化          减少迭代开销           计算密集型
向量化            SIMD并行计算           数组处理
别名分析          解锁更多优化           指针代码
强度削减          用快操作替代慢操作     乘除法

核心原则:优化在保证程序语义不变的前提下,通过减少工作量、提高并行性、改善内存访问模式来提升性能。

 

 

GCC/Clang 优化等级详细对比

一、优化等级总览

text
-O0  ──────────────────────────────────────────  最慢,最易调试
-O1  ████
-O2  ████████████                                ← 推荐生产环境
-O3  ████████████████████
-Os  ██████████ (代码尺寸优先)
-Oz  ████████   (代码尺寸最小, Clang独有)
-Og  ████       (调试友好)                       ← 推荐开发环境
-Ofast ████████████████████████                  ← 危险!破坏IEEE标准
      性能提升 ──────────────────────────────►

二、逐级详细说明

-O0(无优化,默认)

C
// 源代码
int add(int a, int b) {
    int result = a + b;
    return result;
}
asm
; -O0 生成的汇编(x86-64)
add:
    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-20], edi   ; 保存参数a到栈
    mov     DWORD PTR [rbp-24], esi   ; 保存参数b到栈
    mov     edx, DWORD PTR [rbp-20]   ; 读取a
    mov     eax, DWORD PTR [rbp-24]   ; 读取b
    add     eax, edx                   ; a + b
    mov     DWORD PTR [rbp-4], eax    ; 保存到result变量
    mov     eax, DWORD PTR [rbp-4]    ; 读取result
    pop     rbp
    ret
; ↑ 每个变量都老实存到栈上,完全对应源码
text
特点:
✓ 编译速度最快
✓ 调试信息完整,断点/单步完全准确
✓ 变量值可以直接在调试器中查看
✗ 运行速度最慢(多余的内存读写)
✗ 生成代码量最大

适用:日常开发、调试 Bug

-O1(基本优化)

在 -O0 基础上启用以下优化:

text
基本优化集合:
┌─────────────────────────────────────────────┐
│ ✓ 常量折叠          ✓ 死代码消除            │
│ ✓ 基本块合并        ✓ 局部公共子表达式消除  │
│ ✓ 简单内联          ✓ 尾调用优化(部分)      │
│ ✓ 基本寄存器分配优化                        │
└─────────────────────────────────────────────┘
C
// 源代码
int example() {
    int x = 10;
    int y = 20;
    int z = x + y;      // 常量表达式
    if (0) {
        printf("never"); // 死代码
    }
    return z;
}
asm
; -O1 生成的汇编
example:
    mov eax, 30    ; 直接计算出结果
    ret            ; 死代码和中间变量全部消除
text
与 -O0 相比:
✓ 消除明显冗余
✓ 基本寄存器使用优化
✓ 编译时间略增加
~ 调试基本可用,但某些变量可能看不到
✗ 不做激进的循环优化
✗ 不做向量化

适用:对速度有轻微要求,但仍需调试

-O2(标准优化,推荐生产)

在 -O1 基础上增加大量优化:

text
-O2 新增的主要优化:
┌─────────────────────────────────────────────────────┐
│ 循环优化                                            │
│   ✓ 循环不变量外提 (LICM)                          │
│   ✓ 循环强度削减                                    │
│                                                     │
│ 函数优化                                            │
│   ✓ 更激进的内联                                    │
│   ✓ 函数克隆 (针对常量参数)                        │
│                                                     │
│ 分析优化                                            │
│   ✓ 别名分析                                        │
│   ✓ 全局公共子表达式消除                            │
│   ✓ 部分冗余消除 (PRE)                             │
│                                                     │
│ 代码生成                                            │
│   ✓ 指令调度                                        │
│   ✓ 分支预测优化                                    │
│   ✓ if-conversion (分支转无分支)                   │
└─────────────────────────────────────────────────────┘
C
// 循环不变量外提示例
// 源码
for (int i = 0; i < n; i++) {
    arr[i] += sqrt(x) * factor;  // sqrt(x)是循环不变量
}

// -O2 优化后等效代码
double tmp = sqrt(x) * factor;   // 提到循环外
for (int i = 0; i < n; i++) {
    arr[i] += tmp;
}
C
// 分支转无分支 (if-conversion)
// 源码
int abs_val(int x) {
    if (x < 0) return -x;
    return x;
}

// -O2 汇编(无分支跳转)
abs_val:
    mov    eax, edi
    neg    eax
    cmovns eax, edi    ; 条件移动指令,避免分支预测失败
    ret
text
与 -O1 相比:
✓ 性能提升显著(通常 20-50%)
✓ 大多数重要优化都包含
✓ 不破坏 IEEE 浮点标准
✓ 不做可能增大代码体积很多的优化
~ 调试较困难(变量可能被优化掉)
✗ 默认不做向量化(GCC需要-ftree-vectorize)

适用:绝大多数生产环境 ← 首选

-O3(激进优化)

在 -O2 基础上增加:

text
-O3 新增的主要优化:
┌─────────────────────────────────────────────────────┐
│ ✓ 自动向量化 (Auto-vectorization)                  │
│   - 利用 SSE/AVX/NEON 等 SIMD 指令                │
│                                                     │
│ ✓ 更激进的内联 (更大的函数也内联)                  │
│                                                     │
│ ✓ 循环变换                                          │
│   - 循环展开 (Loop unrolling)                      │
│   - 循环向量化                                      │
│   - 循环分裂/融合                                   │
│                                                     │
│ ✓ 推测执行优化                                      │
│ ✓ 更多函数克隆                                      │
└─────────────────────────────────────────────────────┘
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];
}
asm
; -O3 自动向量化后的汇编(AVX2)
; 一次处理 8 个 float
vmovups  ymm0, [rdi]        ; 加载 8 个 float from a
vaddps   ymm0, ymm0, [rsi]  ; 8个float同时相加
vmovups  [rdx], ymm0        ; 存储 8 个 float to c
; 循环步进 8,速度提升 ~8倍
text
-O3 的风险:
┌──────────────────────────────────────────────────┐
│ ⚠ 代码体积可能显著增大(内联、循环展开)        │
│ ⚠ 编译时间明显增加                              │
│ ⚠ 激进内联可能导致指令缓存压力增大              │
│ ⚠ 某些情况下比 -O2 更慢!(代码膨胀)          │
│ ⚠ 向量化可能引入对齐要求                        │
└──────────────────────────────────────────────────┘

适用:科学计算、图形处理、音视频编解码等计算密集型

-Os(优化代码大小)

text
策略:使用 -O2 的大多数优化,但禁用会增大代码体积的优化

启用:所有 -O2 优化中不增大体积的部分
禁用:
  ✗ 循环展开
  ✗ 函数内联(激进部分)
  ✗ 函数克隆
  ✗ 向量化(某些情况)
C
// 对比示例
void process(int* arr, int n) {
    for (int i = 0; i < n; i++) {
        arr[i] = arr[i] * 2 + 1;
    }
}
asm
; -O3(展开后,代码更大但更快)
; 每次处理4个元素
add rdi, 0
...循环展开的代码...

; -Os(紧凑循环)
process:
    test esi, esi
    jle  .done
.loop:
    lea  eax, [rdi*2+1]    ; 紧凑的单次迭代
    mov  [rdi], eax
    add  rdi, 4
    dec  esi
    jnz  .loop
.done:
    ret
text
适用场景:
✓ 嵌入式系统(Flash/ROM 受限)
✓ 共享库(减少加载时间)
✓ 移动端应用(安装包大小)
✓ 缓存受限的环境(小代码=更好的指令缓存命中)

-Oz(最小化代码,Clang 独有)

text
比 -Os 更激进地缩减代码体积

在 -Os 基础上:
✗ 禁用几乎所有内联
✗ 更激进的尺寸削减
✓ 可能使用更短的指令序列(即使更慢)
text
-O2 代码大小:100%
-Os 代码大小:~70-80%
-Oz 代码大小:~60-70%

适用:WebAssembly、固件、极度受限环境

-Og(调试优化,GCC 独有)

text
目标:在保持良好调试体验的前提下,提供一定优化

启用:不影响调试的安全优化
禁用:
  ✗ 变量消除(保持变量可观察)
  ✗ 激进内联(保持调用栈清晰)
  ✗ 代码重排(保持源码行对应)
text
调试质量对比:
         变量可见性  调用栈清晰  断点准确  性能
-O0      ████████   ████████   ████████   ░░
-Og      ███████    ███████    ███████    ██
-O1      █████      █████      █████      ████
-O2      ███        ███        ████       ████████
-O3      ██         ██         ███        ██████████
text
适用:日常开发(替代 -O0,更快的开发构建)
GCC 推荐:开发用 -Og,发布用 -O2

-Ofast(危险优化)

text
= -O3 + 违反标准的浮点优化 + 其他激进假设

额外启用:
  ✓ -ffast-math        浮点重排序(违反 IEEE 754)
  ✓ -fno-trapping-math 忽略浮点异常
  ✓ -ffinite-math-only 假设无 NaN/Inf
  ✓ -fassociative-math 假设浮点结合律(数学上对,计算机上错)
  ✓ -fno-signed-zeros  忽略 -0.0
C
// -Ofast 破坏浮点语义的例子
float sum = 0;
for (int i = 0; i < n; i++)
    sum += arr[i];

// -Ofast 可能重排为:
// (arr[0]+arr[2]) + (arr[1]+arr[3]) + ...
// 浮点加法不满足结合律,结果不同!

// 具体例子
float a = 1e38, b = -1e38, c = 1.0;
float result1 = (a + b) + c;  // = 0 + 1 = 1.0
float result2 = a + (b + c);  // = a + (-1e38+1) ≈ 0(精度损失)
// -Ofast 可能把 result1 算成 result2 的结果
text
⚠ -Ofast 使用场景:
✓ 科学计算(可接受微小误差)
✓ 游戏(不需要精确浮点)
✗ 金融计算(绝对不能用)
✗ 需要精确 NaN/Inf 处理
✗ 任何对浮点精度有要求的场合

三、各等级优化 Pass 对比表

text
优化技术                    O0  O1  O2  O3  Os  Oz  Og
──────────────────────────────────────────────────────
常量折叠                     ✗   ✓   ✓   ✓   ✓   ✓   ✓
死代码消除                   ✗   ✓   ✓   ✓   ✓   ✓   ✓
基本内联                     ✗   ✓   ✓   ✓   ✓   ✗   ✓
寄存器分配优化               ✗   ✓   ✓   ✓   ✓   ✓   ✓
循环不变量外提               ✗   ✗   ✓   ✓   ✓   ✓   ✗
全局公共子表达式消除         ✗   ✗   ✓   ✓   ✓   ✓   ✗
指令调度                     ✗   ✗   ✓   ✓   ✓   ✓   ✗
分支预测优化                 ✗   ✗   ✓   ✓   ✓   ✓   ✗
if-conversion                ✗   ✗   ✓   ✓   ✓   ✓   ✗
自动向量化                   ✗   ✗   △   ✓   △   ✗   ✗
激进循环展开                 ✗   ✗   ✗   ✓   ✗   ✗   ✗
函数克隆                     ✗   ✗   △   ✓   ✗   ✗   ✗
激进内联                     ✗   ✗   ✗   ✓   ✗   ✗   ✗
快速浮点                     ✗   ✗   ✗   ✗   ✗   ✗   ✗

△ = 有限启用

四、性能实测对比

C
// 测试程序:矩阵乘法
void matmul(float* A, float* B, float* C, int N) {
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            for (int k = 0; k < N; k++)
                C[i*N+j] += A[i*N+k] * B[k*N+j];
}
text
N=512 矩阵乘法,Intel i7,GCC 12:

等级    时间(ms)    相对 -O0    代码大小
──────────────────────────────────────
-O0      4820ms       1.0x       较大
-O1       890ms       5.4x       中等
-O2       340ms      14.2x       中等
-O3        95ms      50.7x       较大(向量化)
-Os       420ms      11.5x       最小
-Ofast     88ms      54.8x       较大

注:实际结果因代码和硬件而异

五、如何选择

text
你的场景                        推荐等级
──────────────────────────────────────────────────
日常开发 / 调试 Bug             -O0 或 -Og
追求调试质量 + 一定性能         -Og
生产环境(通用)                -O2  ← 最常用
科学计算 / 音视频 / 游戏        -O3
嵌入式 / 固件 / 包大小敏感      -Os
WebAssembly / 极限压缩          -Oz(Clang)
允许浮点误差的极限性能          -Ofast(谨慎)

六、进阶:组合使用

Bash
# 生产环境推荐组合
gcc -O2 \
    -march=native \          # 针对当前CPU优化
    -flto \                  # 链接时优化(跨文件内联)
    -fprofile-use \          # 使用PGO数据(需先采集)
    program.c

# 调试推荐
gcc -Og \
    -g3 \                    # 最完整调试信息
    -fsanitize=address \     # 内存错误检测
    program.c

# 嵌入式推荐
gcc -Os \
    -ffunction-sections \    # 每个函数单独段(链接器可删除未用函数)
    -fdata-sections \
    -Wl,--gc-sections \      # 链接时删除未用段
    program.c
Bash
# 查看具体启用了哪些优化Pass
gcc -O2 -Q --help=optimizers | grep enabled

# 单独开关某个优化
gcc -O2 -fno-inline program.c          # O2 但禁用内联
gcc -O2 -ftree-vectorize program.c     # 手动开启向量化
gcc -O3 -fno-unroll-loops program.c   # O3 但禁止循环展开

核心建议:

  • 开发期用 -Og(GCC)或 -O0 -g
  • 发布版用 -O2,再根据 profiling 数据决定是否升级到 -O3
  • 不要盲目用 -O3,先测量,再决定

 

posted on 2026-09-03 12:50  GKLBB  阅读(12)  评论(0)    收藏  举报