「JVM 编译优化」编译器优化技术

后端编译(即时编译、提前编译)的目标时将字节码翻译成本地机器码,而难点是输出优化质量较高的机器码;

1. 优化技术概览

PerformanceTacticIndex from OpenJDK Wiki

  • compiler tactics,编译器策略
    • delayed compilation,延迟编译
    • tiered compilation,分层编译
    • on-stack replacement,栈上替换
    • delayed reoptimization,延迟优化
    • program dependence graph representation,程序依赖图表示
    • static single assignment representation,静态单赋值表示
  • speculative (profile-based) techniques,基于性能监控的优化技术
    • optimistic nullness assertions,乐观空值断言
    • optimistic type assertions,乐观类型断言
    • optimistic type strengthening,乐观类型增强
    • optimistic array length strengthening,乐观数组长度增强
    • untaken branch pruning,裁剪未被选择的分支
    • optimistic N-morphic inlining,乐观的多态内联
    • branch frequency prediction,分支频率预测
    • call frequency prediction,调用频率预测
  • proof-based techniques,基于证据的优化技术
    • exact type inference,精确类型推断
    • memory value inference,内存值推断
    • memory value tracking,内存值跟踪
    • constant folding,常量折叠
    • reassociation,重组
    • operator strength reduction,操作符退化
    • null check elimination,空值检查消除
    • type test strength reduction,类型检查退化
    • type test elimination,类型检查消除
    • algebraic simplification,代数简化
    • common subexpression elimination,公共子表达式消除
    • integer range typing
  • flow-sensitive rewrites,数据流敏感重写
    • conditional constant propagation,条件常量传播
    • dominating test detection
    • flow-carried type narrowing,基于流承载的类型缩减转换
    • dead code elimination,无用代码消除
  • language-specific techniques,语言相关的优化技术
    • class hierarchy analysis,类型继承关系分析
    • devirtualization,去虚拟机化
    • symbolic constant propagation,符号常量传播
    • autobox elimination,自动装箱消除
    • escape analysis,逃逸分析
    • lock elision,锁消除
    • lock fusion,所膨胀
    • de-reflection,反射消除
  • memory and placement transformation,内存及代码位置变换
    • expression hoisting,表达式提升
    • expression sinking,表达式下沉
    • redundant store elimination,冗余存储消除
    • adjacent store fusion,相邻存储合并
    • card-mark elimination
    • merge-point splitting,交汇点分离
  • loop transformations,循环变换
    • loop unrolling,循环展开
    • loop peeling,循环剥离
    • safepoint elimination,安全点消除
    • iteration range splitting,迭代范围分离
    • range check elimination,范围查找消除
    • loop vectorization,循环向量化
  • global code shaping,全局代码调整
    • inlining (graph integration),内联
    • global code motion,全局代码外提
    • heat-based code layout,基于热度的代码布局
    • switch balancing,Swith 调整
    • throw inlining
  • control flow graph transformation,控制流图变换
    • local code scheduling,本地代码编排
    • local code bundling,本地代码封包
    • delay slot filling,延迟槽填充
    • graph-coloring register allocation,着色图寄存器分配
    • linear scan register allocation,线性扫描寄存器分配
    • live range splitting
    • copy coalescing,复写聚合
    • constant splitting,常量分裂
    • copy removal,复写移除
    • address mode matching,地址模式匹配
    • instruction peepholing,指令窥孔优化
    • DFA-based code generator,基于正确有限状态机的代码生成

方法内联

// 原始代码
static class B {
    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.get();
    // ...do stuff...
    z = b.get();
    sum = y + z;
}

// 内联后的代码,即时编译实际效果是作用在代码中间表示或机器码之上的,这里只是以 Java 代码演示效果
public void foo() {
    y = b.value;
    // ...do stuff...
    z = b.value;
    sum = y + z;
}

内联可以去除方法调用的成本(如查找方法版本、建立栈帧等),可以为其他哟花建立良好条件;一般是优先级最高的优化手段;

冗余访问消除Redundant Loads Elimination

public void foo() {
    y = b.value;
    // ...do stuff...
    z = y;
    sum = y + z;
}

...do stuff... 不影响 b.value 的情况下,可以将 z=b.value 替换为 z=y,可以不再去访问 b 的局部变量,消除了公共子表达式 b.value(Common Subexpression Elimination);

复写传播Copy Propagation

public void foo() {
    y = b.value;
    // ...do stuff...
    y = y;
    sum = y + y;
}

变量 z 与变量 y 完全相等,是没有必要使用的,可以使用 y 代替 z;

无用代码消除Dead Code Elimination

public void foo() {
    y = b.value;
    // ...do stuff...
    sum = y + y;
}

上述这些优化带来的代码压缩和执行效率提升在实际机器指令上会表现得更明显;

2. 方法内联(Inlining)

最重要的优化技术之一;把目标方法的代码原封不动的复制到发起调用的方法中(实际 JVM 实现会复杂很多),除了消除方法调用的成本(查找方法版本、创建栈帧等),更重要的是为其他优化建立良好基础;

public static void foo(Object obj) {
    if (obj != null) {
        System.out.println("do something");
    }
}

public static void testInline(String[] args) {
    Object obj = null;
    foo(obj);
}

示例中全部是Dead Code,但若没有内联,就无法通过无用代码消除来优化掉这些 Dead Code

按照经典编译原理的优化理论,大多数 Java 方法无法进行内联;只有使用 invokespecial 调用私有方法、示例构造器、父类方法,使用 invokestatic 调用静态方法,使用 invokestatic 调用被 final 修饰的方法,才会在编译期进行解析,其他 Java 方法都属于虚方法,都必须在运行时进行方法接收者的多态选择

如 b.get() 直接内联 b.value,若无上下文,将无法确认 b 的实际类型,从而无法内联到对应方法;

无法内联的解法

  • 类型继承关系分析Class Hierarchy Analysis,CHA),在整个应用程序范围做类型分析(确认当前已加载的类中,接口有哪些实现类、类是否存在子类、子类是否复写某个虚方法等);

  • 不是虚方法,直接进行内联;

  • 是虚方法,向 CHA 查询此方法在当前运行时状态下是否有多台选择;若只有一个版本,可假设应用程序的全貌就是只有一个版本,从而进行守护内联Guarded Inlining),此后 VM 可能加载新的类型从而改变 CHA,因此这种内联属于激进预测性优化,必须预备退路(退回解释状态,或重新编译);若存在多态选择,即时编译器将进行最后一次努力,使用内联缓存Inline Cache)来缩减方法调用开销;

  • 内联缓存Inline Cache),建立在目标方法正常入口之前的缓存,在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,以后进来的每次调用比较其版本;若版本一致(单态内联Monomorphic Inline Cache),可通过缓存调用,仅比不内联的非虚方法调用多了一次类型判断开销;若版本不一致,则退化成超多态内联缓存Megamorphic Inline Cache),相当于真正查找虚方法进行方法分派;

方法内联优化与面向对象的编程方式相矛盾,所以在 Java 语言进行方法内联大多数情况下都是激进优化,而类似的激进优化在高性能 JVM 中极为常见,比如激进优化移除出现概率很小的隐式异常、使用概率很小的分支等;但这些优化都必须具备逃生门(重回解释状态);

3. 逃逸分析(Escape Analysis)

最前沿的优化技术之一;分析对象动态作用域,虽不是直接优化代码的手段,却是其他优化措施的依据与前提(类似继承关系分析);

  • 方法逃逸,一个在方法里定义的对象可能被外部方法引用(作为调用参数传递给其他方法);
  • 线程逃逸,一个对象被跨线程访问(赋值给可能被其他线程访问的实例变量);

逃逸分析的优化用途

  • 栈上分配Stack Allocations

JVM 对堆中分配的对象进行 GC 需要大量计算标记筛选出可回收对象,并进行回收和整理;若一个对象不会发生线程逃逸(实际这类对象的比例很大),这个对象就可能在栈上分配内存,对象可以随方法的结束自动销毁,GC 的压力将大幅下降;

  • 标量替换Scalar Replacement

将一个 Java 对象拆解成若干个用原始类型表示的成员变量;若一个对象不会发生方法逃逸,这个对象就可以被拆散为标量,程序执行就不用创建这个对象,而是直接在栈上创建成员变量(栈上分配,大几率分配到物理机器的高速寄存器中存储),这还可以作为进一步优化的基础; - 标量,无法再分解成更小数据表示的数据,如 JVM 的原始数据类型 int,long 等,以及 reference 类型; - 聚合量Aggregate),一个可以继续分解的数据,如 Java 中的对象;

  • 同步消除Synchronization Elimination),若一个对象不会发生线程逃逸,那么对这个变量的读写同步操作可以被安全的消除掉;

逃逸分析的计算成本(如复杂的数据流敏感的过程间分析)非常高,无法保证逃逸分析带来的性能收益高于它的消耗,JDK 6 才开始支持逃逸分析(Update 23 服务端编译器默认开启);

inline 关键字定义 Java 内联类型可以实现 C# 中值类型相对应的功能,可以令逃逸分析变得简单很多;

逃逸分析模拟演示

// 原始代码
public int test(int x) {
    int xx = x + 2;
    Point p = new Point(xx, 42);
    return p.getX();
}
// 步骤1: 构造函数内联
public int test(int x) {
    int xx = x + 2;
    Point p = point_memory_alloc();     // 在堆中分配 P 对象的示意方法
    p.x = xx;                           // Point 构造函数被内联
    p.y = 42
    return p.x;                         // Point::getX()被内联
}
// 步骤2: 标量替换
public int test(int x) {
    int xx = x + 2;
    int px = xx;                        // p.x 与 p.y 不会发生方法逃逸,可以直接替换为标量 px、py
    int py = 42;
    return px;
}
// 步骤3: 无效代码消除
public int test(int x) {
    return x + 2;                       // py 对运行效果无影响,为 Dead Code,可消除
}

逃逸分析相关参数

-XX:+DoEscapeAnalysis,手动开启逃逸分析;
-XX:+PrintEscapeAnalysis,查看分析结果;
-XX:+EliminateAllocations,开启标量替换;
-XX:+EliminateLocks,开启同步消除;
-XX:+PrintEliminateAllocations,查看标量的替换情况;

大型程序中实施逃逸分析可能出现效果不稳定的情况,或分析过程耗时却无法有效判别出非逃逸对象,导致性能(即时编译的收益)下降;

4. 公共子表达式消除(Common Subexpression Elimination)

语言无关的经典优化技术之一;对于公共子表达式,没必要重复计算,可以直接用前面计算的结果替换;

  • 公共子表达式,如果一个子表达式已经被计算过,且表达式中变量的值不曾发生变化,那这个子表达式就可以当做公共子表达式;基本代码块中的为局部公共子表达式Local Common Subexpression Elimination),跨基本代码块则可称为全局公共子表达式Global Common Subexpression Elimination);

公共子表达式消除演示

// 原始代码
int d = (c * b) * 12 + a + (a + b * c);

javac 直译的 Class 格式效果

iload_2     // b
imul        // 计算b*c
bipush 12   // 推入12
imul        //计算(c*b)*12
iload_1     //a
iadd        //计算(c*b)*12+a
iload_1     //a
iload_2     //b
iload_3     //c
imul        //计算b*c
iadd        //计算a+b*c
iadd        //计算(c*b)*12+a+a+b*c
istore 4

即时编译优化效果

// 将 c*b 和 b*c 用 E 表示,消除公共子表达式
int d = E * 12 + a + (a + E);

// 代数简化(Algebraic Simplification)
int d = E * 13 + a + a;

5. 数组边界检查消除(Array Bounds Checking Elimination)

语言相关的经典优化技术之一;Java 语言作为一门动态安全的语言,会自动对数组的读写访问索引合法性做检查,当超出地址范围,则抛出 java.lang.ArrayIndexOutOfBoundsException,这对软件开发很友好,但对 JVM 却是一个性能负担;若能在编译期根据数据流分析判定索引一直在数组边界内,就可以消除数组上下边界的检测,从而节省很多次条件判断操作;

类似的消除还可以发生在空指针检查(NullPointException)、除数为零检查(ArithmeticException)、自动装箱消除Autobox Elimination)、安全点消除Safepoint Elimination)、消除反射Dereflection)等;针对这些检查的消除方式,还可以采用隐式异常处理的思路;

隐式异常处理示例

// 原始伪代码
if(foo != null) {
    return foo.value;
} else {
    throw new NullPointException();
}

// 隐式异常消除后的伪代码
try {
    return foo.value;
} catch (segment_fault) {
    uncommon_trap();
}

JVM 注册一个 Segment Fault 信号的异常处理器(uncommon_trap(),针对进程层面的异常处理器,与 try-catch 的线程级异常处理器不同),当 foo 不为空,可以省去判空的开销;但若 foo 真为空,会转到异常处理器(涉及进程从用户态转内核态,结束后再转用户态)恢复中断并抛出 NullPointException,这将远比一次判空要慢;借助 VM 在运行期收集的性能监控信息,判定 foo 极少为空时,采用这样的优化方式会更值得;


上一篇:「JVM 编译优化」提前编译器
下一篇:「JVM 编译优化」Graal 编译器

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值