原文链接:https://blog.csdn.net/en_joker/article/details/90265974
Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除去虚拟机解释执行字节码时额外消耗时间的原因外,还有一个很重要的原因就是虚拟机设计团队几乎把对所有优化措施都集中在了即时编译器之中(在JDK 1.3之后,Javac就去除了-O选项,不会生成任何字节码级别的优化代码了),因此一般来说,即时编译器产生的本地代码会比Javac产生的字节码更加优秀(本地代码与字节码两者是无法直接比较的,准确的说应当是指:由编译器优化得到的本地代码与由编译器解释字节码后实际执行的本地代码之间的对比)。下面,将介绍一些HotSpot虚拟机的即时编译器在生成代码时采用的代码优化技术。
在Sun官方的Wiki上,HotSpot虚拟机设计团队列出了一个相对比较全面的、在即时编译器中采用的优化技术列表(见下表),其中有不少经典编译器的优化手段,也有许多针对Java语言(准确的说是针对运行在Java虚拟机上的所有语言)本身进行的优化技术,本节将对这些技术进行概括性的介绍,在后面几节中,再挑选若干重要且典型的优化,与读者一起看看优化前后的代码产生怎样的变化。
类型 | 优化技术 |
---|---|
编译器策略 (compiler tatics) | 延迟编译(delayed compilation) 分层编译(tiered compilation) 栈上替换(on-stack replacement) 延迟优化(deplayed reoptimization) 程序依赖图表示(program dependence graph representation) 静态单赋值表示(static single assignment representation) |
基于性能监控的优化技术 (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) |
数据流敏感重写 (flow-sensitive rewrites) | 条件常量传播(conditional constant propagation) 基于流承载的类型缩减转换(flow-carried type narrowing) 无用代码消除(dead code elimination) |
语言相关的优化技术 (language-specific techniques) | 类型继承关系分析(class hierarchy analysis) 去虚拟机化(devirtualization) 符号常量传播(symbolic constant propagation) 自动装箱消除(autobox elimination) 逃逸分析(escape analysis) 锁消除(lock elision) 锁膨胀(lock coarsening) 消除反射(de-reflection) |
内存及代码位置变换 (memory and placement transformation) | 表达式提升(expression hoisting) 表达式下沉(expression sinking) 冗余存储消除(redundant store elimination) 相邻存储合并(adjacent store fusion) 交汇点分离(merge-point splitting) |
循环变化 (loop transformations) | 循环展开(loop unrolling) 循环剥离(loop peeling) 安全点消除(safepoint elimination) 迭代范围分离(range check elimination) 循环向量化(loop vectorization) |
全局代码调整 (global code shaping) | 内联(inlining) 全局代码外提(global code motion) 基于热度的代码布局(heat-based code layout) Switch调整(switch balancing) |
控制流图变换 (control flow graph transformation) | 本地代码编排(local code scheduling) 本地代码封包(local code bundling) 延迟槽填充(delay slot filling) 着色图寄存器分配(graph-coloring register allocation) 线性扫描寄存器分配(linear scan register allocation) 复写聚合(copy coalescing) 常量分裂(constant splitting) 复写移除(copy removal) 地址模式匹配(address mode matching) 指令窥孔优化(instruction peepholing) 基于确定有限状态机的代码生成(DFA-based code generator) |
上述的优化技术看起来很多,而且从名字看都显得有点“高深莫测”,虽然实现这些优化也许确实有些难度,但大部分技术理解起来都并不困难。为了消除读者对这些优化技术的陌生感,下面举一个简单的例子,即通过大家熟悉的Java代码来展示其中几种优化技术是如何发挥作用的(仅使用Java代码来表示而已)。首先从原始代码开始,如下所示。
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源码之上的。
上面的代码已经非常简单了,但是仍有许多优化的余地。
- 第一步进行方法内联(Method Inlining),方法内联的重要性要高于其他优化措施,他的主要目的有两个,一是去除方法调用的成本(如建立栈帧等),二是为其他优化建立良好的基础,方法内联膨胀之后可以便于更大范围上采取后续的优化手段,从而获取更好的优化结果。因此,各种编译器一般都会把内联优化放在优化序列的最靠前位置。内联后的代码如下所示。
public void foo() {
y = b.value;
// ... do stuff...
z = b.value;
sum = y+z;
}
第二步进行冗余访问消除(Redundant Loads Elimination),假设代码中间注释掉的“dostuff......”所代表的操作不会改变b.value的值,那就可以把“z=b.value”替换为“z=y”,因为上一句“y=b.value”已经保证了变量y与b.value是一致的,这样就可以不再去访问对象b的局部变量了。如果把b.value看作是一个表达式,那也可以把这项优化看成是公共子表达式消除(Common Subexpression Elimination),优化后的代码如下所示。
public void foo() {
y = b.value;
// ... do stuff...
z = y;
sum = y+z;
}
- 第三步我们进行复写传播(Cop Propagation),因为在这段程序的逻辑中并没有必要使用一个额外的变量“z”,他与变量“y”是完全相等的,因此可以使用“y”来代替“z”。复写传播之后程序如下所示。
public void foo() {
y = b.value;
// ... do stuff...
y=y;
sum = y+y;
}
- 第四步我们进行无用代码消除(Dead Code Elimination)。无用代码可能是永远不会被执行的代码,也可能是完全没有意义的代码,因此,他又形象的称为“Dead Code”,在上面代码中,”y=y“是没有意义的,把他消除后的程序如下所示。
public void foo() {
y = b.value;
// ... do stuff...
sum = y+y;
}
经过四次优化之后,上面代码和第一个代码所达到的效果是一致的,但是前者比后者省略了许多语句(体现在字节码和机器码指令上的差距会更大),执行效率也会更高。编译器的这些优化技术实现起来也许比较复杂,但是要理解他们的行为对于一个普通的程序员来说是没有困难的。