You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
140 lines
5.5 KiB
140 lines
5.5 KiB
5 years ago
|
---
|
||
|
从 final 能够“提升”程序性能,谈编译优化
|
||
|
---
|
||
|
|
||
|
#### 前言
|
||
|
|
||
|
或许你不止一次听说过 fianl 能够提升程序性能,那真的是这样的嘛?
|
||
|
|
||
|
本节先通过讲解主流编译优化的手段,了解完编译优化的思想和内在原理,相信你对这个问题就会做出一个很好的回答。
|
||
|
|
||
|
#### 概述
|
||
|
|
||
|
对于 Java 代码的编译,分为前端编译和后端编译。
|
||
|
|
||
|
前端编译是指通过 Javac 工具,将 Java 代码转化为字节码的过程。既然 Javac 负责字节码的生成,那肯定就会有一些通用的优化手段。比如常量折叠、自动装拆箱、循环遍历、条件编译等。其次,还有使用 StringContactFactory 对 “+” 的重载的统一入口等。
|
||
|
|
||
|
后端编译则是指 JVM 内置的解释器和即时编译器(C1、C2)。
|
||
|
|
||
|
JVM 在对代码执行的优化可以分为运行时优化和即时编译器(JIT)优化。运行时优化主要是解释执行和动态编译通用的一些机制,比如说锁机制(如偏斜锁)、内存分配机制(如 TLAB)等。除此之外,还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存(inline cache,用于优化虚方法调用的动态绑定)。
|
||
|
|
||
|
JVM 的即时编译器优化是指将热点代码以方法为单位转换成机器码,直接运行在底层硬件之上。它采用了多种优化方式,包括静态编译器可以使用的如方法内联、逃逸分析,也包括基于程序运行 profile 的投机性优化(speculative/optimistic optimization)。
|
||
|
|
||
|
#### 编译优化
|
||
|
|
||
|
1. 前端编译优化
|
||
|
- 常量折叠
|
||
|
- 条件编译
|
||
|
2. 后端编译优化
|
||
|
- 锁机制
|
||
|
- 内存分配机制
|
||
|
- Intrinsic 机制
|
||
|
- 方法内联
|
||
|
- 逃逸分析
|
||
|
|
||
|
2. 循环优化
|
||
|
6. 向量化
|
||
|
7. 其他
|
||
|
|
||
|
#### 常量折叠
|
||
|
|
||
|
常量折叠发生在 Javac 的编译过程中的语义分析过程中的标注检查阶段。
|
||
|
|
||
|
```java
|
||
|
String abc = "a" + "b" + "c";
|
||
|
```
|
||
|
|
||
|
以上代码经过常量折叠之后变为:
|
||
|
|
||
|
```java
|
||
|
String abc = "abc";
|
||
|
```
|
||
|
|
||
|
#### 条件编译
|
||
|
|
||
|
```java
|
||
|
public void show() {
|
||
|
if (1 + 1 != 2) {
|
||
|
//do something
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Java 语言中条件编译的实现,也是 Java 语言的一颗语法糖,根据布尔常量值的真假,编译器会把分支中不成立的代码块消除掉,这一工作将在编译器解语法糖阶段完成。
|
||
|
|
||
|
#### 锁机制
|
||
|
|
||
|
JDK 1.6 对锁的实现引入了大量的优化,包括自旋锁、锁消除、偏向锁,轻量级锁来减少锁操作的开销。
|
||
|
|
||
|
锁主要存在四种状态:依次是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
|
||
|
|
||
|
- 自旋锁和自适应自旋
|
||
|
|
||
|
在很多应用中,锁定状态只会持续很短的时间,为了这么一点时间就去挂起、恢复线程,不值得。我可以让等待线程执行一定次数的循环,在循环中去获取锁,这就称为自旋锁。它可以节省系统切换线程的消耗,但是仍占用处理器。在之后又引入自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
|
||
|
|
||
|
- 偏向锁
|
||
|
|
||
|
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。
|
||
|
|
||
|
- CAS
|
||
|
|
||
|
#### 内存分配机制
|
||
|
|
||
|
对象优先在 TLAB 上分配内存。在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配内存空间的任务就等同于把一块确定大小的内存从 Java 堆中划分出来。内存分配方式有两种,一种是指针碰撞,一种是空闲列表。
|
||
|
|
||
|
对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改了一个指针所指向的位置,在并发情况下也并不是线程安全的。解决这个问题有两种办法,一种是对内存分配进行同步处理,另外一种就是把内存分配在 TLAB 上,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。
|
||
|
|
||
|
#### Intrinsic 机制
|
||
|
|
||
|
或者叫做内建方法,对应的注解为 HotSpotIntrinsicCandidate,就是针对特别重要的基础方法,JDK 团队直接提供定制的实现,利用汇编或者编译器的中间表达式编写,然后 JVM 会直接在运行时进行替换。
|
||
|
|
||
|
这么做的理由有很多,例如,在不同体系结构的 CPU 在指令等层面存在着差异,定制才能充分发挥出硬件的能力。我们日常使用的典型字符串操作、数组拷贝等基础方法,HotSpot 虚拟机都提供了内建实现。
|
||
|
|
||
|
在 Math 类刚好有这样的一个例子:
|
||
|
|
||
|
```java
|
||
|
public static void main(String[] args) {
|
||
|
|
||
|
long startTime = System.nanoTime();
|
||
|
for (int i = 0; i < 1000; i++) {
|
||
|
int result = Math.max(0, i);
|
||
|
}
|
||
|
long endTime = System.nanoTime();
|
||
|
System.out.println("内建方法执行耗时:");
|
||
|
System.out.println(endTime - startTime);
|
||
|
|
||
|
startTime = System.nanoTime();
|
||
|
for (long i = 0; i < 1000; i++) {
|
||
|
long result = Math.max(0L, i);
|
||
|
}
|
||
|
endTime = System.nanoTime();
|
||
|
System.out.println("非内建方法执行耗时:");
|
||
|
System.out.println(endTime - startTime);
|
||
|
|
||
|
}
|
||
|
|
||
|
内建方法执行耗时:
|
||
|
92170
|
||
|
非内建方法执行耗时:
|
||
|
97459
|
||
|
```
|
||
|
|
||
|
#### 方法内联
|
||
|
|
||
|
|
||
|
|
||
|
#### 循环优化
|
||
|
|
||
|
循环优化有两种:
|
||
|
|
||
|
- 循环无关代码外提
|
||
|
- 循环展开
|
||
|
|
||
|
#### 方法内联
|
||
|
|
||
|
getter/setter 为例。
|
||
|
|
||
|
#### 其他
|
||
|
|
||
|
还有一些优化方式,比如:
|