更多内容可以访问我的个人博客。
Java编译优化技术
Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除了虚拟机解释执行字节码时额外消耗时间的原因外,还有一个很重要的原因就是——虚拟机设计团队把几乎所有的代码优化措施都集中在了即时编译器中,因此一般来说,即时编译器产生的本地代码会比Javac产生的字节码更优秀。
以下代码优化变换建立在代码的某种中间或机器码上,绝不是建立在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;
}
1. 方法内联 Method Inlining
方法内联的重要性要高于其他优化措施。它的主要目的有两个:①去除方法调用的成本(如建立栈帧等) ② 为其他优化建立良好基础(方法内联膨胀之后可以便于在更大范围上采取后续的优化手段)
方法内联之后的代码
public void foo(){
y = b.value;
/*...do stuff...*/
z = b.value;
sum = y + z;
}
2. 冗余访问消除 Redundant Loads Elimination
假如代码中do stuff中所代表的操作不会改变b.value和y的值,就可以把“z = b.value”替换为"z = y",这样就可以不再去访问对象b的局部变量了。
冗余访问消除后的代码
public void foo(){
y = b.value;
/*...do stuff...*/
z = y;
sum = y + z;
}
3. 复写传播 Copy Propagation
在这段代码逻辑中没有必要使用一个额外的变量“z”,它与“y”是完全等价的。
复写传播后的代码
public void foo(){
y = b.value;
/*...do stuff...*/
y = y;
sum = y + y;
}
4. 无用代码消除 Dead Code Elimination
无用代码可能是永远不会被执行的代码,也可能是完全没有意义的代码,也被称为“Dead Code”
无用代码消除后的代码
public void foo(){
y = b.value;
/*...do stuff...*/
sum = y + y;
}
再介绍以下几项最具代表性的优化技术:
语言无关的经典优化技术之一:公共子表达式消除
语言相关的经典优化技术之一:数组范围检查消除
最重要的优化技术之一:方法内联
最前沿的优化技术之一:逃逸分析
5.公共子表达式消除
含义:如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就成为了公共子表达式,无需再次计算,用之前的结果代替即可。
举例:
int d = (c * b) * 12 + a + (a + b * c);
⇩(通过JIT编译器,检测到b *c 与 c *b是一样的表达式,且计算中b与c的值不变)
int d = E * 12 + a + (a + E);
⇩(还可能(取决于在哪种VM的编译器上以及具体的上下文而定)进行另一种优化:代数化简
int d = E * 13 + a * 2;
6. 数组边界检测擦除
Java 是一门动态安全的语言,对数组的访问也不像C、C++本质上是裸指针操作。对开发者来说,可以避免大部分的溢出攻击(运行时错误 ArrayIndexOutOfBoundsException),即使没有编写专门的防御代码。对VM来说,每次数组元素的读写都带有一次隐含的条件判定操作,是一种性能负担。
①数组下标是常量:编译期根据数据流分析确定length,确定没越界,执行时就无需判断
②数组访问发生在循环中:判断循环变量的取值范围在 [0,length )之内,就可以把整个循环中的数组上下界检查消除
(相当于把“运行期”检查提到了“编译期”完成)
- 隐式异常优化:
另外Java中大量的安全检查,如空指针(NullPointException)、除数为零(ArithmeticException),需要Java做大量检查判断——隐式异常处理。Java中空指针和除数为零都采用了这种思路。
比如如下代码:
if(foo != null){ return foo.value; }else{ throw new NullPointException(); }
使用隐式异常优化之后:
try{ return foo.value; }catch(segment_fault){ uncommon_trap(); }
优点:这样当foo不为空时,对value的访问不会额外消耗一次对foo的判空开销
缺点:当foo为空的时候,需要转入异常处理器中恢复并抛出NullPointException异常,开销更大。
7. 逃逸分析
前沿技术,并不直接优化代码,而是为其他优化手段提供依据的分析技术。
逃逸分析的基本行为就是分析对象动态作用域。
-
方法逃逸:一个对象在方法中被定义,可能被外部方法所引用,例如作为调用参数传递到其他方法中。
-
线程逃逸:可能被外部线程访问到,例如赋值给类变量或可以在其他线程中访问的实例变量。
若能证明一个对象不会逃逸到方法或线程之外,则可能为这个这个变量进行一些高效的优化。
①栈上分配:对象分配在栈上,占用的内存可以随栈帧出栈而销毁,减少GC压力。
②同步消除:既然对象不会逃逸出线程,无法被其他线程访问,则可消除对其的同步措施。
③标量替换:“标量”是指一个数据已经无法再分解成更小的数据来表示(如Java原始数据类型),与之相对的叫“聚合量”(如对象)。“标量替换”是指程序执行时不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替(成员变量则分配再栈上)。
- ”逃逸分析技术尚未成熟“:仍不能保证逃逸分析的性能收益一定高于它的消耗。
Java与C/C++的编译器对比
-
Java:即时编译器(JIT)
-
C/C++:静态编译器
Java即时编译器的劣势:(这些性能上的劣势都是为了换取开发效率,如“动态安全”、“动态扩展”、“垃圾回收”等)
① JIT占用用户程序的运行时间,具有很大的时间压力,能提供的优化手段也严重受制于编译成本。不敢引入大规模的优化技术,而编译的时间成本在静态优化编译器中并不是主要的关注点。
②Java是动态的类型安全语言,需由VM确保程序不会违反语言语义 或访问非结构化内存。(需进行空指针检查、数组边界检查…)
③ 使用虚方法频率远大于C/C++,意味着对方法接收者进行多态选择的频率远大于C/C++。导致“方法内联”难度高。
④ Java可动态扩展,运行时可改变类的继承关系,进行全局优化难。
⑤ Java中对象堆上分配,只有方法中局部变量在栈上分配,而C/C++则有多种内存分配方式(栈、堆上都有可能)
Java即时编译器的优势:
① 在C/C++中,“别名分析”难度远高于Java
② C/C++的所有优化都在编译器,以运行期性能监控为基础的优化,它都无法进行