晚期(运行期)优化——编译优化技术


为什么编译方式执行本地代码比解释方式执行字节码?

  1. 虚拟机解释执行字节码需要额外消耗时间(难道是从字节码转换成机器码的时间,我有想到了池这种技术)
  2. 虚拟机团队对代码所有的优化措施都集中在即时编译器之中

一、优化技术概览

类型 优化技术
编译器策略
基于性能监控的优化技术

基于证据的优化技术

下面举一个例子说明一下:

在这里插入图片描述
下面是他们的优化步奏,需要注意的是,优化都是发生在代码的某种中间表示或机器码之上

  • 第一步:final方法内联
    public void foo() {
    	y = b.value;
    	...do stuff...
    	z = b.value;
    	sum = y + z;
    }
    
  • 第二步:消除冗余访问
    public  void foo() {
    y = b.value;
    ...do stuff...
    z = y;//b.value与y值一样
    sum = y + z;
    }
    
  • 第三步:复写传播
    public  void foo() {
    y = b.value;
    ...do stuff...
    y = y;//z值和y一样,没有必要创建多余的变量
    sum = y + y;
    }
    
  • 第四部:无用代码消除
    //y=y是没有意义的
    public  void foo() {
    y = b.value;
    ...do stuff...
    sum = y + y;
    }
    

二、公共子表达式消除

什么叫做公共子表达式消除?
如果对于表达式E,如果之前计算过了,并且该表达式中的变量值没有被修改,那么下次遇到这个表达式时,就没有必要再次进行计算了。如:

//其中c*b就相当于E,当下次遇到E时(也就是b*c),就不用再次计算E的值了
int d=(c*b)*12+a+(a+b*c);

三、数组边界检查消除

Java中,对与数组的访问,每一次都要判断数组是否越界。虽然这带来了安全,但是每一次都存在隐含的条件判定语句,这成为了一种性能负担。

于是就考虑在哪些情况下消除数组边界检查。如,array[3],如果索引是个常量,在编译期就可以确定array.length(),进而判断是否越界,于是在执行时就不用判断了。还有循环体进行数组访问,使用循环变量作为索引,在编译的时候判断循环变量是否在[0,array.length)之间,如果在就可以去掉数据边界检查。

从更高角度看,比如NullPointerException、ArthmeticException(除零错误),C++中它不管,还是要执行,执行炸了就退出。然而Java中要先判断执行条件是否成立,不成立就不执行。相对于在编译器完成检查之外,还有另一种在运行时检查的方式——隐式异常处理。

下面伪代码表示访问foo.value的过程。

if(foo!=null)
{
	return foo.value;
}else{
	throw new NullPointException();
}

经过隐式异常优化之后:

try{
	return foo.value;
}catch(segment_fault){
	uncommon_trap();
}

这样的话就可以省去一次判断的开销了。然而,当foo真为空时,抛出异常所需时间比一次判断多得去了。当foo很少为空时,这种方法是值的。对于这种情况,我们的虚拟机足够聪明去选择一个最优的优化方案!

之前面试,面试过问我为什么选择java,不选择C++。当时我是这样回答的:java简单啊!
现在想来,好肤浅啊。选择java还是C++要看你的业务,公司的需求。如果你为了追求安全性,可以选择Java,如果你最求性能,可以选择C++(前提是你C++会写,不然非但性能得不到,还会影响安全)。


四、方法内联

4.1 为什么要进行方法内联

  • 它可以消除方法调用的成本
  • 为其他优化手段建立良好的基础

4.2 例子

下面这个例子,如果不做内联,无法进行无用代码优化。

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);
}

分成两个方法看,两个方法都是有意义的。然而,内联之后你就会发现那都是无用代码。所以内联为其他优化手段建立了良好的基础

4.3 内联和虚方法之间的矛盾

由于虚方法的确定是在运行是才知道的,而内联是在方法运行之前进行的,我们又想对虚方法进行内联。因此内联和虚方法之间就这样产生矛盾了。

4.4 解决办法

处理这个问题,需要用到类型继承关系分析(CHA)内联缓存,还有逃生门

是否为虚方法
非虚方法,直接内联
在CHA中查找此方法是否只有一个版本
进行守护内联,这是一种激进优化,需要设置逃生门
如果这个方法的接受者的继承关系发生变化,就进行逃生
第一次调用方法时,进行方法内联,以后每一次调用这个方法时都判断接受者是否改变
如果改变,就查找虚方法表进行方法分派

五、逃逸分析

5.1 什么叫做方法逃逸、线程逃逸

方法逃逸:

方法内部定义的变量,被外部方法引用。如作为调用参数传递到外部方法中

线程逃逸:

被外部线程访问的就叫做线程逃逸。如赋值给类变量或可以在其他线程中访问的实例变量。

5.2 没有方法逃逸、线程逃逸之后可以干什么

  • 栈上分配。因为对象不被其他方法访问,当方法结束后,就可以随栈帧的消失而自动释放内存
  • 同步消除。都不会发生线程逃逸了,当然可以将多余的同步控制消除掉。
  • 标量替换。java虚拟机中的原始数据类型称为标量。将方法访问到的对像中的成员变量恢复成原始类型来访问称为标量替换。这样就可以不用创建对象了

需要注意的是,这项技术还不成熟。默认是没有开启的。

猜你喜欢

转载自blog.csdn.net/wobushixiaobailian/article/details/83998816