吃透方法内联

Posted 小孟的coding之旅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了吃透方法内联相关的知识,希望对你有一定的参考价值。

那么在这个基础上即时编译器还做了很多的优化措施,从而进一步的提升性能。本课时我们一起来探讨方法内联,它就是即时编译器的优化措施之一。

方法内联

 

代码优化技术有很多,实现这些优化也很有难度,但是大部分还是比较好理解的。为了便于介绍,我们先从一段简单的代码开始,看看虚拟机是如何实现方法内联代码优化。

 

public class InlineDemo {
    private static int add1(int x1,int x2,int x3,int x4){
        return add2(x1,x2)+add2(x3,x4);
    }
    public static int add2(int x1,int x2){
        return x1+x2;
            }
}

 

上面的代码非常的简单,在 add1 方法里面调用了两次 add2 方法。你想编译器会怎么样优化这段代码呢?

 

我们知道调用方法需要经过压栈和出栈的操作,进入方法的时候会向栈里面压入一个元素,返回的时候,会从栈里面弹出这个元素。

 

压栈和出栈操作都是有开销的,比如压栈的时候,得往栈里面存数据,所以存在内存的开销。同时压栈和出栈也都是需要时间,所以还有时间的开销。

 

如果代码的调用次数不多,那么压栈和出栈的开销倒也无所谓了。但是假设代码调用非常的频繁,比如每秒要掉到两万次,累积下来的开销还是比较客观的。

 

是不是有什么办法去优化的?当然是有的,JVM 它会自动识别热点方法,并自动的去进行方法内联。所谓方法内联就是把目标方法的代码复制到发起调用的方法里面去,去避免掉真实的方法调用。比如像上面所写代码,经过方法内联之后。内联优化后的代码如下:

 

 private static int addInline(int x1,int x2,int x3,int x4){
        return x1+x2+x3+x4;
    }

 

也就是说会自动的把 add1 方法和 add2 方法合并到一起去,从而减小调压栈和出栈的操作。

 

方法内联的重要性要高于其它优化措施。方法内联的目的主要有两个,一是去除方法调用的成本(比如建立栈帧),二是为其他优化建立良好的基础,方法内联膨胀之后可以便于更大范围上采取后续的优化手段,从而获得更好的优化效果。因此,各种编译器一般都会把内联优化放在优化序列的最前面。

 

方法内联的条件

 

那么需要注意的是,使用方法内联它是有条件的:

 

第一:你的方法体要足够小,如果方法体太大,JVM 是不会内联的,默认情况下对于热点方法,如果方法体小于 325 字节都会去尝试内联,你也可以使用这个参数:-XX:FrequentInlineSize 去修改方法的大小,有关 JVM 如何找到热点方法,上一课时已经详细探讨过了。对于非热点方法,如果你的方法体小于 35 字节,也会去尝试内联,你也可以使用这个参数:-XX:MaxInlineSize 去修改方法的大小,这是方法内联的第一个条件。

 

第二:被调用的方法能在运行时方法的实现可以被唯一确定,这样的方法才可能会被内联。那么对于 static、private 以及 final修饰的方法,JIT 都是可以唯一确定实现的。但是对于 public 修饰的实例方法,它指向的实现可能是这个方法自身或者是父类或者是子类的方法实现,因为有多态的存在。那么这种情况下,只有当即时编译器能够唯一确定方法的实现时,才可能完成内联。这是方法内联的第二个条件。

 

方法内联的主要点

 

那么根据这两点条件,我们可以总结出使用方法内联的几个注意点:

 

第一:我们在写代码的时候,应当尽量避免在一个方法里面编写大量的代码,让方法体尽量的小一点。

 

第二:我们应当尽量使用 final、private static 等等关键字修饰方法,避免因为多态要对方法做额外的类型检查,而且很可能检查之后发现还没有办法内联,因为没有办法唯一确定方法的实现。

 

第三:在某些场景下,可以通过设置 JVM 参数去减少热点的阈值或者修改方法体大小的阈值,从而让更多的方法去进行内联。比如配置 FrequentInlineSize 或者是 MaxInlineSize。

 

使用方法内联可能带来的问题

 

下面来探讨一下使用方法内联可能带来的问题。

 

首先要强调的是,方法内联不是万能药,它也是有缺点的,内联本质上是一种用空间换时间的玩法,也就是即时编译器在编译期间把方法调用连接起来,从而减少进栈和出栈的开销。但是经过内联之后的代码会变多,而增加的代码量又取决于方法的调用次数以及方法本身的大小。

 

所以在一些极端场景下,内联甚至可能会导致 CodeCache 的溢出,那么 CodeCache 是热点代码的一个缓存区,即时编译器编译后的代码以及本地方法代码会存放在 CodeCache 里面。那么这块空间是比较有限的,JDK 8 默认情况下只有 240 MB。这块空间一旦溢出,甚至可能会导致 JVM 放弃编译运行而退化人解释执行模式。

 

那么目前你只要知道有这么一回事儿就可以了。在后面的章节,关于 CodeCache 的调优会详细探讨的。最后我们总结一下和方法内联相关的 JVM 参数。

 

参数 默认 说明
-XX:+PrintInlinging - 打印内联详情,请参数需和 -XX:+UnlockDiagnosticVMOptions 配合使用
-XX:+UnlockDiagnosticVMOptions - 打印JVM诊断相关信息
-XX:MaxInlineSize=n  35 如果非热点方法的字节码超过该值,则无法内联,单位字节
-XX:FreqInlineSize=n  325 如果热点方法的字节码超过该值,则无法内联,单位字节
-XX:InlineSmallCode=n  1000 目标编译后生成的机器码代销大于该值则无法内联,单位字节
-XX:MaxInlineLevel=n  9 内联方法的最大调用帧数(嵌套调用的最大内联深度)
-XX:MaxTrivialSize=n  6 如果方法的字节码少于该值,则内联单位字节
-XX:MinlnliningThreshould=n  250 如果目标方法的调用次数低于该值,则不去内联
-XX:LiveNodeCountInliningCutoff=n  40000 编译过程中最大活动节点数( IR 节点)的上限,仅对 C2 编译器有效
-XX:InlineFrequencyCount=n  100 如果方法的调用点(call site)的执行次数超过该值,则触发内联
-XX:MaxRecursiveInlineLevel=n 1 递归调用大于该值就不内联
-XX:InlineSynchronizedMethods 开启 是否开启内联同步方法

这个应该是比较全的内联参数了。

 

下面来利用这些参数做一个实验,我们打印一下案例中方法的内联详情。

 

public class InlineDemo1 {
    private static final Logger LOGGER = LoggerFactory.getLogger(InlineDemo1.class);
    public static void main(String[] args) {
        long cost=compute();
        LOGGER.info("执行花费{}",cost);
    }
    private static long compute(){
        long start=System.currentTimeMillis();
        int result=0;
        Random random=new Random();
        for (int i=0;i<100000000;i++){
            int a=random.nextInt();
            int b=random.nextInt();
            int c=random.nextInt();
            int d=random.nextInt();
            result=add1(a,b,c,d);
        }
        long end =System.currentTimeMillis();
        return end-start;
    }
    private static int add1(int x1,int x2,int x3,int x4){
        return add2(x1,x2)+add2(x3,x4);
    }
    private static int add2(int x1,int x2){
        return x1+x2;
    }
}

 

在 IDEA 中配置:

 

 

 

值得注意的是:-XX:+UnlockDiagnosticVMOptions 参数必须 -XX:+PrintInlining 参数之前。

 

执行成功后,控制台输出的结果,我们看一下方法有沒有内联。

 

 

 

通过运行的结果来看,我们的方法被内联,并且是热点方法,同时方法体的字节数也打印出来了。目前 add1 方法需要12 byte,add2 方法需要 4 byte,并且执行花费时间为:5095 ms。

 

下面我们再来让这个方法不做内联,来对比一下方法,内联和不内联之间的性能差异。通过这个参数:-XX:FreqInlineSize=1 设置为不内联,还记得这个参数是做什么的吧,它用来指定热点方法的字节码的字节数值,如果超过这个值的话,就不去内联。现在我们的方法都是超过 1 byte 的,所以把它设成 1 就不会内联。

 

通过执行程序之后,可以看到当不内联的时候,执行花费时间为:6847 ms。

不难发现,如果方法频繁调用的时候,内联对性能的影响还是比较客观的。不过在实际项目中,个人并不建议随意配置这些 JVM 参数。尽管这些参数非常的灵活,也非常的丰富。我个人建议正常情况下使用默认值即可,因为现在 JVM 已经非常智能了。

一般来说,我们可以忽略掉这些细节,把优化的工作交给 JVM 去完成。但是当项目出现性能瓶颈的时候,你要能够想到有一种调优机制叫做方法内联,必要能够有能力去进行相关的调优。

以上是关于吃透方法内联的主要内容,如果未能解决你的问题,请参考以下文章

2C++ 的升级

<code> vs <pre> vs <samp> 用于内联和块代码片段

一文吃透 Kotlin 中眼花缭乱的函数家族...

一文吃透 Kotlin 中眼花缭乱的函数家族...

一文吃透 Kotlin 中眼花缭乱的函数家族...

Web前端一文带你吃透HTML(下篇)