当前位置: 首页 > news >正文

编译器优化

编译器优化

  • 编译器优化的目标和原则
    • 优化目标:
    • 优化原则:
  • 不同的角度看待编译器优化
    • 从编译器阶段角度
      • GCC/LLVM编译器架构划分
      • 前端优化(语法层优化):
      • 中端(IR优化)
      • 后端(目标架构相关优化)
      • 同时结合中端和后端
    • 从其他角度看待编译器优化
      • 静态优化 vs 动态优化
      • 局部优化 vs 全局优化
      • 语言无关 vs 有关
      • 基于假设优化
  • 编译器的分析支持手段
  • 具体C/C++优化技术介绍

本文是一篇编译器优化的总览,不会给太多细节,细节会在之后的文章中补全。

编译器优化的目标和原则

优化目标:

  • 性能优化(最常见):提升执行速度。
  • 体积优化:减少可执行文件大小。
  • 功耗优化:尤其在嵌入式系统或移动设备中。

    可以简单的认为少做事就是减少功耗。

  • 编译时间优化:提升开发效率。
  • 调试信息保留:某些优化要避免影响可调试性。

优化原则:

  • 保持语义等价性:不能因为优化改变了代码本身的逻辑

  • 权衡收益与代价:

    优化前评估优化可能导致的增加编译时间、代码变大、调试困难等代价,与明显提升运行效率等收益,两者对比之后是否值得做这项优化。

  • 面向热路径优化(Profile-guided Optimization, PGO):

    80/20原则,程序 80% 的运行时间都花在 20% 的代码上。因此编译器会用各种方法是不是热路径,从而进行精确优化。

不同的角度看待编译器优化

从编译器阶段角度

GCC/LLVM编译器架构划分

典型 C/C++ 编译器(如 GCC、Clang/LLVM)的结构分为:

  1. 前端(Frontend):词法分析、语法分析,生成 AST。
  2. 中端(Middle-end):语义分析、生成中间表示(IR)。
  3. 后端(Backend):将 IR 转换为目标机器码。

前端优化(语法层优化):

  • 常量折叠
  • 死代码删除
  • 内联展开
  • 返回值优化

中端(IR优化)

IR是一种语言无关的中间表示,在该阶段已经去除了不同高级语言之间的差异,IR优化对于不同高级语言而言做法都是同样的。
数据流分析支持的优化

  • 公共子表达式消除(CSE)
  • 复制传播(Copy Propagation)
  • 死代码消除(DCE)
  • 循环不变代码外提(LICM)
  • 强度削弱(Strength Reduction):如将 i*8 转为 i<<3。
    控制流优化
  • 基本块合并、重排(Block Merging / Reordering)
  • 空转移删除(Trivial Jump Removal)
  • 条件传播与跳转优化
    SSA-based 优化
  • 引入 Static Single Assignment form 之后:
  • 高效的数据依赖分析
  • 实现 GVN(全局数值冗余消除)
  • 精准的范围分析和别名分析

后端(目标架构相关优化)

后端是将IR中间表示转化为机器代码,因此在这部分做的优化是跟目标机器相关的优化。

  • 寄存器分配(Register Allocation)
  • 指令调度(Instruction Scheduling)
  • 指令选择优化(Instruction Selection)
  • 延迟槽填充(针对 RISC 架构)
  • 函数和数据布局优化(Function Reordering)

同时结合中端和后端

循环优化

  • 循环展开(Unrolling)
  • 循环合并 / 分裂
  • 循环交换(Loop Interchange)
  • 循环向量化(Loop Vectorization):自动使用 SIMD 指令。
  • 预取指令插入(Software Prefetch)

从其他角度看待编译器优化

静态优化 vs 动态优化

  1. 静态优化:编译时完成,不依赖运行信息。
  2. 动态优化:运行时(JIT)完成,如 Java HotSpot、LLVM ORC JIT。

目前C/C++基本都是静态优化,因为C/C++的代码是直接编译成机器码进行执行的,编译完成时候执行流程已经确定了,无法在运行时根据执行情况动态调整机器码。

说是基本是因为目前有伪动态优化技术,可以通过 PGO(Profile-Guided Optimization)来模拟一部分动态信息驱动的静态优化,说白了就是编译成机器码后运行一段时间收集运行信息,根据运行信息再次进行编译(实际上还是静态优化)。

局部优化 vs 全局优化

  • 局部优化:局限于单个基本块。?
  • 全局优化:跨基本块、跨函数甚至模块。?

语言无关 vs 有关

  • 大部分 IR 层优化是语言无关的,如 LLVM 的 mem2reg。
  • 但前端可以做语言相关优化,如 C++ 的内联构造函数消除(RVO)。

基于假设优化

基于“程序不会这么干”的语义假设来允许更激进的优化。
这种优化思想的本质是
如果我们相信程序员遵守某些规范,那我们就可以做得更大胆。

这些优化往往在编译器中带有“不确定性”和“大胆性”:

  • 如果假设成立 → 提升性能
  • 如果假设被程序员违反 → 未定义行为 or fallback(deoptimization)
优化名假设效果
严格别名优化(Strict Aliasing)不同类型指针不指向同一内存缓存变量值、不重复读取内存
函数内联后去虚(Devirtualization)虚函数的实际类型唯一可内联虚函数调用
条件分支预测优化条件总是偏向某一方向(来自 PGO 或启发)指令重排、填延迟槽
const 推导(Const Propagation)常量不在运行时改变变量替换为立即数,消除运算
内联展开 + 类型推导模板调用总是特定类型对模板函数展开、特化

编译器的分析支持手段

决定是否优化的关键依据

  1. 数据流分析(Data Flow Analysis)

    确定变量的定义、使用范围、常量性等

    决定如:死代码删除、常量传播、CSE 是否可行

  2. 控制流图(CFG)分析

    判断哪些分支是冗余的

    控制流重排、条件传播优化都依赖 CFG

  3. 静态单赋值形式(SSA)

    所有变量只赋值一次,方便追踪数据来源

    有助于做 alias analysis、GVN、Loop Optimization

  4. 别名分析(Alias Analysis)

    判断两个指针是否可能指向相同内存

    如果不别名 → 可做更 aggressive 的内联、预取、载入消除

  5. 循环分析(Loop Analysis)

    判断循环是否可展开、可向量化(SIMD)

    如 Loop Unrolling、Loop Fusion、LICM 都依赖此分析

具体C/C++优化技术介绍

挑选几个比较有代表性的

  1. 常量折叠
  • 原理

    如果表达式在编译期就可以知道值是多少,就直接在编译期计算表达式的值。

  • 示例

const int a = 3 * 4 + 5;     // 3 * 4 + 5会折叠为 17
constexpr int b = func(2);   // 如果 func 是 constexpr 函数
  • 作用

编译期直接计算表达式值,减少运行期计算开销(因为在编译期已经计算了)。

  • 建议

    • 多使用 constexpr、const 和 consteval(C++20)
    • 避免在常量表达式中调用运行时函数
    • 避免写太复杂、编译器无法推导的表达式
  1. 常量传播
  • 原理
    如果能够确定某个变量是常量,就会用它的值替换掉变量本身,从而简化表达式、生成更高效的代码。

    常量折叠和常量传播不太一样,两者都是编译期确定值

    • 常量折叠是针对表达式的优化技术,在编译期直接计算出表达式的值,计算值替换表达式,避免在编译期进行计算。
    • 常量传播时针对常量的优化技术,发现常量的值在编译期时可知的,那么就把这个值赋给使用这个常量的其他变量/常量,也就是把值传播出去
  • 示例

    const int x = 5;
    int y = x + 3; // 编译器会直接把这行代码优化成 int y = 8;因为x是const变量,且值是编译器可知的
    
  • 作用

    作用描述
    消除冗余计算把计算提前执行(在编译期),减少运行时负担
    配合死代码删除(DCE)传播后分支/循环可静态确定,从而整段代码被删除
    触发更多高级优化传播后变量确定 → 可进行循环展开、指令合并等
    提高代码可预测性和局部性消除不必要的变量间接访问,有利于 CPU 优化
  • 建议
    不要尝试修改一个const变量的值

const int x = 10;
int* ptr = (int*)&x;
*ptr = 20;
printf("x = %d\n", x); // 输出结果可能是10,也可是20,取决于译器的优化策略,如果常量传播,则x的值会编译期直接替换成10。
printf("*ptr = %d\n", *ptr); // 输出结果为20
  1. 死代码删除
  • 原理

    识别并移除永远不会影响程序结果的代码,比如不可能执行的分支、不会被使用的变量赋值等。也就是移除无效代码段。

    通常依赖于:

    • 控制流分析(Control Flow Analysis)
    • 数据流分析(Data Flow Analysis)
      来判断哪些语句是“无效的”。
  • 示例

int main() {int x = 42;  // 死代码:x 没被使用int y = 0;if (false) {y = 5;   // 死代码:永远不会执行}return 0;
}
  • 作用
作用描述
减小代码体积去除无用指令,减少目标代码大小
提升运行效率减少不必要的变量、指令、内存访问
为后续优化铺路删除死变量后,可进一步触发其他优化(如寄存器分配更好)
清理“调试遗留代码”编译器可以自动消除开发时临时遗留逻辑
  • 建议
    使用 const, constexpr 明确表达不变,编译器更容易判断代码是否无效。
  1. 内联展开
  • 原理

编译器将函数调用的地方,直接展开为函数体代码,避免函数调用的开销(如栈帧建立、跳转等)。

  • 示例
inline int add(int a, int b) {return a + b;
}int main() {int x = add(1, 2);  // 会被替换为 int x = 1 + 2;
}
  • 作用

    • 减少函数调用开销(如栈帧建立、函数跳转)
    • 使调用点附近的优化成为可能(如常量折叠、死代码删除)。

      例如上述例子add(1, 2)可能会被直接替换为1 + 2,进而可以进行常量折叠在编译期直接计算出3。

    • 增强编译器对整体逻辑的掌握,利于进一步优化。

      如果只是函数调用,编译器只看到了需要调用某一个函数,但是函数内部的信息是不明确的额,而内联则是将函数内部处理逻辑直接在调用点处展示出来,对编译器来说自然是知道的信息越多约能进行更细致的优化。

  • 建议

  1. 返回值优化
  • 原理

    Return Value Optimization(RVO).

    Name Return Value Optimization(NRVO).
    对于返回值的具名和不具名进行区分,则有RVO和NRVO这两种区别。

    编译器在函数返回局部对象时,避免构造临时对象和拷贝/移动操作,直接在调用方分配的内存中构造对象

  • 示例

struct Big {Big() { std::cout << "ctor\n"; }Big(const Big&) { std::cout << "copy\n"; }
};Big make() {Big b;return b;  // 若触发 NRVO,这里不会打印 copy
}
  • 作用

    • 避免不必要的拷贝或移动构造,提升性能。

    打一个不是很准确的比分,RVO就像是下边这样

       int add(int a, int b){int res = a + b; return res;}int main() {int res = add( 1 + 2); return 0;}// 做RVO等同于把res传到add中void add(int& res, int a, int b) { res = a + b; }int main(){ int res = 0; add(res, 1, 2); return 0;}
    
  • 建议

    • 优先返回未命名临时对象:return Obj();(RVO)

      因为未命名的临时对象,几乎一定会出发RVO

    • 返回命名对象时,结构尽量简单、单一返回点(NRVO)
    • 避免在 return 中涉及多个对象选择、复杂分支(会阻止 NRVO)。因为这样编译器会不知道要选择哪一个对象进行NRVO。

    从编译器的角度,要实现NRVO必须在编译期间就知道要返回的是哪一个对象,这样对象所在的内存才能确定,自然是越简单越好。

  1. 尾调用优化
  • 原理

    当一个函数在 返回之前最后一步就是调用另一个函数,编译器可以将当前函数的栈帧替换为被调用函数的栈帧,避免栈增长,提升性能并防止栈溢出。

    尾递归就是尾调用中最后一个函数是调用自己,形成递归。

    尾递归优化,编译器实际上可能把递归函数转换为循环实现。

  • 示例

int factorial(int n, int acc = 1) {if (n == 0) return acc;return factorial(n - 1, acc * n);  // 尾递归
}
  • 作用

    • 消除递归带来的栈空间开销
    • 支持“无限”递归深度,适用于数学函数、状态机等
  • 建议

    • 递归函数写成尾递归形式(返回时直接 return 调用,不做额外计算)
  • 详细内容
    C++ 中的尾调用优化TCO:原理、实战与汇编分析

  1. 公共子表达式消除(CSE, Common Subexpression Elimination)
  • 原理

    将多次出现的等价表达式只计算一次,存为临时变量,避免重复计算。

  • 示例

    int a = x * y + 5;
    int b = x * y + 10;
     CSE
    int t = x * y;
    int a = t + 5;
    int b = t + 10;
    
  • 作用

    • 减少指令次数,降低运算资源消耗
    • 提升浮点运算、图形处理等计算密集型场景的效率
  • 建议

    • 编写表达式时避免无意义的重复计算
    • 避免副作用表达式(如带状态函数)以便编译器识别为纯表达式
    • 注意浮点不精确场景下的不可合并

      因为浮点计算不是严格的“数学等价”,会有一定的误差,因此编译器可能不会合并,除非加上一些编译参数,例如-ffast-math

  1. 严格别名
  • 原理
    编译器假设不同类型的指针不会指向同一内存地址,从而允许更激进的优化(如寄存器缓存、不刷新内存等)。

  • 示例

    float f = 1.0f;
    int* p = (int*)&f;  // 违反 strict aliasing
    
  • 作用

    • 提高优化空间:可认为 floa t* 与 int* 指向不同数据
    • 使得缓存、重排序、寄存器保留更安全
  • 建议

    • 避免使用 reinterpret_cast 或 (int)&x 等违反别名规则的写法
    • 使用 memcpy 或标准库安全类型转换
    • 若必须使用 alias,使用 attribute((may_alias)) 或 -fno-strict-aliasing 关闭该优化(降低优化强度)
  1. 自动 SIMD(Auto-Vectorization / 自动向量化)
  • 原理

编译器自动将数据并行循环转换为SIMD 指令(如 SSE/AVX),一次处理多个数据,提高性能。

就是编译器发现在循环中第i个数据的值跟第j个的值无关,,就自动改成并行处理。

  • 示例

    for (int i = 0; i < n; ++i)a[i] = b[i] + c[i];  // → 编译器自动转成 SIMD 加法
    
  • 作用

    • 显著提升数值密集任务性能(图像处理、物理模拟等)
    • 减少循环次数、加速运算
  • 建议

    • 编写简单、无依赖的循环结构
    • 避免循环中有条件分支或跨依赖
    • 使用 -O2 -ftree-vectorize 或 -march=native 使编译器自动尝试 SIMD
    • 可用 #pragma GCC ivdep / #pragma omp simd 强制提示并行性
  1. 数据预取(Prefetching)
  • 原理

通过编译器/CPU 确定未来要用的数据地址,提前加载到 cache,减少因 cache miss 导致的等待。

  • 示例

    for (int i = 0; i < N; ++i) {__builtin_prefetch(&a[i + 16]);  // 手动预取未来访问的数据,提示编译器把a[i + 16]加到缓存中。a[i] = a[i] + 1;
    }
    

    __builtin_prefetch 是GCC 和 Clang 提供的内建函数(builtin function),用于手动插入“预取指令”(prefetch instruction)来优化缓存访问。

  • 作用

    • 减少内存访问延迟
    • 提高缓存命中率
  • 建议

    • 大批量线性访问时可受益
    • 编译器通常能自动判断并插入预取指令
    • 手动预取适合访问跨度大或复杂数据结构

相关文章:

  • 445场周赛
  • DeepSeek技术解析:开源大模型的创新突围之路
  • 在esp-idf中发现找不到头文件
  • linux编译安装nginx
  • 药房智慧化升级:最优成本条件下开启地市级医院智慧医疗新变革
  • 【weaviate】分布式数据写入之LSM树深度解析:读写放大的权衡
  • 【力扣 中等 C】983. 最低票价
  • (LeetCode 面试经典 150 题 ) 189. 轮转数组(字符串、双指针)
  • [linux] Ubuntu 24软件下载和安装汇总(自用)
  • Linux安全基石:Shell运行原理与权限管理系统解读
  • 【Docker基础】Docker容器管理:docker run及其参数详解
  • Python 使用 Requests 模块进行爬虫
  • 学习设计模式《十四》——组合模式
  • dijkstra(迪杰斯特拉)算法详解
  • 阿里云CentOS系统搭建全攻略:开启云端技术之旅
  • bash的配置文件,source
  • 云函数调测、部署及日志查看
  • VSCode性能调优:从卡顿到丝滑的终极方案
  • 颠覆传统接口测试!用 Streamlit + SQLite + GPT 打造可视化自动化平台
  • 计算鱼眼相机的内参矩阵和畸变系数方法
  • 江苏省建设工程协会网站/百度一下你就知道了 官网
  • 做房产必知的发布房源网站/百度账号官网
  • asp网站栏目修改/做app推广去哪找商家
  • 阿克苏网站开发/什么是营销
  • 上海最繁华的五个区/aso安卓优化公司
  • 自建购物网站/沈阳百度快照优化公司