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

怎么进行你的代码优化 编译器怎么优化你的代码

概述

在编程世界中,处处都是代码优化。

以下所述的众多优化中,有些是编译器会帮我们做的优化,有些是我们在程序编写中就可以进行的代码优化。

常量折叠

在代码中可能会出现一些字面量,例如:

int seconds_of_two_hours = 2 * 60 * 60; // 两个小时是多少秒?

编译器可能会对这段代码进行优化:在编译器进行语法分析时,直接计算出 2 * 60 * 60 这个“常量表达式”的结果,并使用该结果直接进行替代,以减少运行时的计算开销:

int seconds_of_two_hours = 7200; // <-- 2 * 60 * 60

在代码中的这些字面量看似是“需要避免的不良编程习惯”,但是在特定场合下,它们极大提高了代码的可读性(例如上述被编译器优化前的代码 2 * 60 * 60 很直观地阐述了该值在真实世界中的物理含义,如果直接使用 7200 可能会让代码阅读者摸不清头脑。因此,如果编译器能做这个优化,程序员既可以写出易读的程序,又不必担心性能受影响。

代码内联

可以直接将「函数调用」替换为「函数体」

主要目的:一、去除方法调用的成本(如查找方法版本、建立栈帧等);二、为其他优化建立良好的基础(函数内联膨胀之后可以便于更大范围上进行后续的优化手段)。

但如果对一个方法体行数较多的方法进行内联,可能会导致代码的膨胀( n 个单行的函数调用可能会变成 n×k 行的代码,其中假设函数体有 k 行代码)。因此,当需要进行函数内联时,尽量选择方法体较小的、函数调用频繁的函数进行内联

// 优化前

inline int add_one(int num) { return num + 1; } // inline 关键字会“建议“而非“强制“编译器进行内联

void func() {
    int num = 0;
    num = add_one(num);
    num = add_one(num);
    num = add_one(num);
    std::cout << num << std::endl;
}

// 优化后

void func() {
    int num = 0;
    num = num + 1;
    num = num + 1;
    num = num + 1;
    std::cout << num << std::endl;
}

值得一提的是,Java 中默认的实例方法是虚方法,而对于一个虚方法,编译器静态地去做内联的时候很难确定应该使用哪个方法版本。

为了解决虚方法的内联问题,JVM 引入了类型继承关系分析(Class Hierarchy Analysis,CHA) 技术,用于“确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息”。

于是,在 JVM 遇到虚方法进行内联时,可以向 CHA 查询此方法在当前程序状态下是否真的有多个目标版本可供选择:【情况一】如果查询到只有一个版本,那就可以假设“应用程序的全貌就是现在运行的这个样子”来进行内联,即“守护内联”(Guarded Inlining)。

然而,由于 Java 程序是动态连接的,说不定在程序运行的某个时刻有新的类型会被 JVM 所加载,从而改变 CHA 结论,因此,可以说 Java 的这种内联属于“激进预测性优化”,必须预留好“当假设不成立时的退路(Slow Path)”。

【情况二】假如,JVM 向 CHA 查询的结果是该方法确实有多个版本的目标可供选择,那 JIT 还将进行“最后一次努力”,使用“内联缓存(Inline Cache)”缩减方法调用的开销。

代码移动

在对一个容器进行遍历时,通常可以使用 for-each 语法进行遍历,但有的时候需要用到当前元素在容器中的索引时,可能就会写出如下代码:

// 优化前

void for_each(const std::vector<int> &nums) {
    for (int i = 0; i < nums.size(); ++i) {
        do_something();
    }
}

在某些场景下,容器的长度是不会改变的,但是在每次 for 循环时都要进行结束条件 i < nums.size() 的判断,导致 nums.size() 进行重复计算,效率较低。

因此,可以使用「代码移动」的技巧,将容器的长度提前计算(作为循环中的“常量”而非每次都要计算的“变值“):

// 优化后

void for_each(const std::vector<int> &nums) {
    // 将每次都要计算的 nums.size() 提前到 size = static_cast<int>(nums.size())
    for (int i = 0, size = static_cast<int>(nums.size()); i < size; ++i) {
        do_something();
    }
}

分支预测

if 等分支指令在实际程序运行时所消耗的时长比其他指令更高,且其大大增加了 CPU 对代码进行并行化的难度。

而分支预测算法能在一定程度上帮助 CPU 去减少特定场景下的分支判断,从而提高程序的效率。

在 C++ 20 中,可以使用 [[likely]][[unlikely]] 进行辅助。

减少不必要的内存引用

相比于位运算、数值运算等操作,内存引用操作(解引用等)具有较长的执行时间(虽然宏观来看并不长,但由于其触发频率之高,是值得考虑优化的点)。

// 优化前

void func(int *num) {
    *num += get_num_0(); // 一次内存引用
    *num += get_num_1(); // 两次内存引用
    *num += get_num_2(); // 三次内存引用
    *num += get_num_3(); // 四次内存引用
}

可以通过「创建临时变量」的操作,将上方四次内存引用减少到一次:

// 优化后
void func(int *num) {
    int temp = 0;
    temp += get_num_0();
    temp += get_num_1();
    temp += get_num_2();
    temp += get_num_3();
    *num = temp;         // <-- 唯一一次内存引用
}

使用位运算

“位运算“在绝大多数编程语言中都是效率最高的运算操作,我们能发现有许多算法库都是运用了大量的位运算技巧来替代可读性可能更高的加减乘除等运算。

公共子表达式消除

如果一个表达式 E E E 之前已经被计算过了,并且从先前的计算到现在 E E E 中所有变量的值都没有发生变化,那么 E E E 的这次出现就称为“公共子表达式”。

没有必要进行公共子表达式的重复计算,只需要用前面计算过的表达式结果进行代替即可。

例如有以下一段代码:

int d = (c * b) * 12 + a + (a + b * c);

当编译器检测到 b * cc * b 等价时,可能会提取为公共子表达式 E = b * c

int d = E * 12 + a + (a + E);

编译器还可能根据具体的上下文进行代数化简(Algebraic Simplification),在 E E E 本来就有乘法运算的前提下,将表达式变为:

int d = E * 13 + a + a;

复写传播

在一段代码中可能会没有必要使用一个额外的变量,例如上方代码中的 z,因此可以使用 y 直接代替 z

// 优化前

void func() {
    y = b.value;
    /* do something that do not change the value of v */
    z = y;        // <-- 没有必要使用「变量 z」
    sum = y + z;
}

// 优化后

void func() {
    y = b.value;
    /* do something that do not change the value of v */
    y = y;
    sum = y + y;
}

无用代码删除

无用代码(Dead Code)可能是“永远不会被执行的代码”,也可能是“完全没有意义的代码”,比如上方代码中的 y = y,完全可以被消除掉:

void func() {
    y = b.value;
    /* do something that do not change the value of v */
    sum = y + y;
}

逃逸分析

逃逸分析(Escape Analysis)是 JVM 中比较前沿的优化技术,其与 CHA 一样,并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术

逃逸分析会分析对象动态作用域,当一个对象在方法里被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这叫方法逃逸;其可能被外部线程所访问到,这叫线程逃逸;从“不逃逸”“方法逃逸”到“线程逃逸”,成为对象由低到高的不同逃逸程度。

如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低,则可能为这个对象实例采取不同程度的优化。

【优化方式一:栈上分配】如果确定一个对象不会逃逸出线程之外,那么可以考虑让其在栈上分配,对象所占用的内存可以随栈帧出栈而销毁。而且一般来说完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁,进而极大降低 GC 子系统的压力。

栈上分配(Stack Allocation)指的是在栈上为对象分配内存,而非使用 malloc/new 等指令在堆上请求空间进行内存分配

【优化方式二:标量替换】如果逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象是可以被拆散的,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替

标量替换(Scalar Replacement)指的是将一个“聚合量(Aggregate)”递归替换为若干个标量(原始数据类型,int、long 等数值类型及 reference 类型等)

【优化方式三:同步消除】如果逃逸分析能够确定一个变量不会逃逸出线程(即无法被其他线程访问),那么这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉

数组边界检查消除

C/C++ 将数据元素的索引视作“裸指针操作”,这样直接的操作具有较高的性能,但是一旦发生数组越界,那将是一个未定义行为(undefined behavior),即 C/C++ 将“防止数组越界”的任务交给程序员。

但在 Java 等语言中,当进行数组元素访问时,系统将会自动进行上下界的范围检查,否则就会抛出一个运行时异常 java.lang.ArrayIndexOutOfBoundsException。这对软件开发者较为友好 —— 因为有兜底的操作,但是对于拥有大量数组访问的程序代码,每次访问操作都附带一些附加安全保证是较大的性能负担

“数组边界检查“并非必要要进行的操作,只要在编译期根据数据流分析确定数组的长度及判断数组下标访问没有越界,那么在运行期就不需要进行额外的判断了。

例如,数组访问发生在循环之中,且程序使用循环变量来进行数组的访问,那么,只要编译器通过数据流分析就可以判定循环变量的取值永远不会越界,就看消除掉数组的边界检查。

参考资料

  1. 《深入理解计算机系统》
  2. 《深入理解 Java 虚拟机》
  3. 编译器优化过程具体是做了些什么,优化后的程序速度能提高多少百分比?

相关文章:

  • vue实战-Search模块开发(大体步骤)
  • C#基础--特殊的集合
  • 吴恩达2022机器学习_第二部分高级学习算法笔记
  • DGL教程
  • FAST-LIO2代码解析(五)
  • 苦卷28天,阿里P8给我的Alibaba面试手册,终于成功踹开字节大门
  • Vue:v-on、v-bind、v-model、@click、:model用法以及区别(附代码实例)
  • 手写Sping IOC(基于Setter方法注入)
  • 一、SpringBoot前置(从0搭建Maven项目)
  • 宝藏教程:超详细一条龙教程!从零搭建React项目全家桶
  • 网络编程学习总结3
  • 欧姆龙CP1H如何进行PLC远程编程及数据采集
  • CSGO Bway电竞ETERNAL FIRE可以参加BLAST FALL,但MOUZ却错过了
  • 收获tips
  • Vue3+elementplus搭建通用管理系统实例十二:使用通用表格、表单实现对应功能
  • 网络传输文件的问题
  • Django 博客开发教程 8 - 博客文章详情页
  • Docker入门(二) - Dockerfile
  • httpie使用详解
  • Java 11 发布计划来了,已确定 3个 新特性!!
  • JAVA 学习IO流
  • Koa2 之文件上传下载
  • Python学习之路13-记分
  • Spring Cloud中负载均衡器概览
  • 不上全站https的网站你们就等着被恶心死吧
  • 如何优雅地使用 Sublime Text
  • 微服务入门【系列视频课程】
  • 微信小程序开发问题汇总
  • 为视图添加丝滑的水波纹
  • 一、python与pycharm的安装
  • 以太坊客户端Geth命令参数详解
  • 怎么将电脑中的声音录制成WAV格式
  • C# - 为值类型重定义相等性
  • 进程与线程(三)——进程/线程间通信
  • 树莓派用上kodexplorer也能玩成私有网盘
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​中南建设2022年半年报“韧”字当头,经营性现金流持续为正​
  • #laravel 通过手动安装依赖PHPExcel#
  • ()、[]、{}、(())、[[]]命令替换
  • (3)nginx 配置(nginx.conf)
  • (C语言)fread与fwrite详解
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (Matalb时序预测)PSO-BP粒子群算法优化BP神经网络的多维时序回归预测
  • (PWM呼吸灯)合泰开发板HT66F2390-----点灯大师
  • (求助)用傲游上csdn博客时标签栏和网址栏一直显示袁萌 的头像
  • (图)IntelliTrace Tools 跟踪云端程序
  • (一)WLAN定义和基本架构转
  • (源码版)2024美国大学生数学建模E题财产保险的可持续模型详解思路+具体代码季节性时序预测SARIMA天气预测建模
  • (转)shell中括号的特殊用法 linux if多条件判断
  • .net core 依赖注入的基本用发
  • .NET/C# 检测电脑上安装的 .NET Framework 的版本
  • .Net8 Blazor 尝鲜
  • /var/spool/postfix/maildrop 下有大量文件
  • @EnableConfigurationProperties注解使用
  • @KafkaListener注解详解(一)| 常用参数详解