0%

方法内联

方法内联:在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。

方法内联只发生在即时编译器中。

方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化,它是编译优化里最为重要的一环。

以getter/setter为例,如果没有方法内联,在调用getter/setter时,程序需要保存当前方法的执行位置,创建并压入用于getter/setter的栈、访问字段、弹出栈帧,最后再恢复当前方法的执行。而内联了对getter/setter的方法调用后,上述操作仅剩字段访问。

方法内联的过程

在C2中,方法内联是在解析字节码的过程中完成的。每当触碰到方法调用字节码时,C2将决定是否需要内联该方法调用。如果需要内联,则开始解析目标方法的字节码。

即时编译器首先解析字节码,并生成IR图,然后再该IR图上进行优化。优化是由一个个独立的优化阶段(optimization phase)串联起来的。每个优化阶段都会对IR图进行转换。最后即时编译器根据IR图的节点以及调度顺序生成机器码。

同C2一样,Graal也会在解析字节码的过程中进行方法调用的内联。此外,Graal还拥有一个独立的优化阶段,来寻找指代方法调用的IR节点,并将之替换为目标方法的IR图。


除了将被调用方法的IR图节点复制到调用者方法的IR图中,即时编译器还需额外完成三项操作:

  • 第一,被调用方法的传入参数节点,将被替换为调用者方法进入方法调用时所传入参数对应的节点。
  • 第二,在调用者方法的IR图中,所有指向原方法调用节点的数据依赖将重新指向被调用方法的返回节点。如果被调用方法存在多个返回节点,则生成一个Phi节点,将这些返回值聚合起来,并作为原方法调用节点的替换对象。
  • 第三,如果被调用方法将抛出某种类型的异常,而调用者方法恰好有该异常类型的处理器,并且该异常处理器覆盖这一方法调用,那么即时编译器需要将被调用方法抛出异常的路径,与调用者方法的异常处理器相连接。

经过方法内联之后,即时编译器将得到一个新的IR图,并且在接下来的编译过程中对这个新的IR图进行进一步的优化。

Java编译器(不是即时编译器)将final静态字段编译为常量值,并且在字节码中直接使用这些常量值,而非读取静态字段。IR图可以进一步优化(死代码消除)

方法内联的条件

方法内联能够触发更多的优化。通常而言,内联越多,生成代码的执行效率越高。然而对即时编译器来说,内联越多,编译时间也就越长,而程序达到峰值性能的时刻也将被推迟。

此外,内联越多也将导致生成的机器码越长。在JVM里,编译生成的机器码会被部署到Code Cache之中。这个Code Cache有大小限制(JVM参数-XX:ReservedCodeCacheSize)。生成的机器码越长,越容易填满Code Cache,从而出现Code Cache已满,即时编译已被关闭的警告信息(CodeCache is full. Compiler has been disabled)。

即时编译器不会无限制的进行方法内联。(自动拆箱总会被内联、Throwable类的方法不能被其他类中的方法所内联。参考JDK源代码)。

  • 首先,由-XX:CompileCommand中的inline指令指定的方法,以及由@ForceInline注解的方法(仅限于JDK内部方法),会被强制内联。而由-XX:CompileCommand中的dontinline指令或exclude指令(表示不编译)指定的方法,以及由@DontInline注解的方法(仅限JDK内部方法),则始终不会被内联。
  • 其次,如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化,或者目标方法是native方法,都将导致方法调用无法内联。
  • 再次,C2不支持内联超过9层的调用(虚拟机参数-XX:MaxInlineLevel),以及1层的直接递归调用(虚拟机参数-XX:MaxRecursiveInlineLevel)。

    如果方法a调用了方法b,而方法b调用了方法c,那么b为a的1层调用,c为a的2层调用。

  • 最后,即时编译器将根据方法调用指令所在的程序路径的热度,目标方法的调用次数以及大小,以及当前IR图的大小来决定方法调用能否被内联。

总体来说,即时编译器中的内联算法更青睐于小方法。

参数名 默认值 说明
-XX:InlineSmallCode 2000 如果目标方法已被编译,且其生成的机器码大小超过该值,则无法内联
-XX:MaxTrivialSize 6 如果方法的字节码大小少于该值,则直接内联
-XX:MinInliningThreshold 250 如果目标方法的调用次数低于该值,则无法内联
-XX:InlineFrequencyCount 100 如果方法调用指令执行次数超过该值,则认为是热点代码
-XX:MaxInlineSize 35 如果非热点方法的字节码大小超过该值,则无法内联
-XX:FreqInlineSize 325 如果热点方法的字节码大小超过该值,则无法内联
-XX:LiveNodeCountInlineCutoff 40000 编译过程中IR节点数目的上限

总结

方法内联是指,在编译过程中,当遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。

即时编译器既可以在解析过程中替换方法调用字节码,也可以在IR图中替换方法调用IR节点。这两者都需要将目标方法的参数以及返回值映射到当前方法来。

方法内联有许多规则。除了一些强制内联以及强制不内联的规则外,即时编译器会根据方法调用的层数、方法调用指令所在的程序路径的热度、目标方法的调用次数及大小,以及当前IR图的大小来决定方法调用能否被内联。

虚拟机参数-XX:+PringInlining打印编译过程中的内联情况。