You cannot 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
9.2 KiB
Markdown

2 years ago
# 20 | 方法内联(上)
在前面的篇章中,我多次提到了方法内联这项技术。它指的是:在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。
方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译优化里最为重要的一环。
以getter/setter为例如果没有方法内联在调用getter/setter时程序需要保存当前方法的执行位置创建并压入用于getter/setter的栈帧、访问字段、弹出栈帧最后再恢复当前方法的执行。而当内联了对getter/setter的方法调用后上述操作仅剩字段访问。
在C2中方法内联是在解析字节码的过程中完成的。每当碰到方法调用字节码时C2将决定是否需要内联该方法调用。如果需要内联则开始解析目标方法的字节码。
> 复习一下即时编译器首先解析字节码并生成IR图然后在该IR图上进行优化。优化是由一个个独立的优化阶段optimization phase串联起来的。每个优化阶段都会对IR图进行转换。最后即时编译器根据IR图的节点以及调度顺序生成机器码。
同C2一样Graal也会在解析字节码的过程中进行方法调用的内联。此外Graal还拥有一个独立的优化阶段来寻找指代方法调用的IR节点并将之替换为目标方法的IR图。这个过程相对来说比较形象一些因此今天我就利用它来给你讲解一下方法内联。
```
方法内联的过程
public static boolean flag = true;
public static int value0 = 0;
public static int value1 = 1;
public static int foo(int value) {
int result = bar(flag);
if (result != 0) {
return result;
} else {
return value;
}
}
public static int bar(boolean flag) {
return flag ? value0 : value1;
}
```
上面这段代码中的foo方法将接收一个int类型的参数而bar方法将接收一个boolean类型的参数。其中foo方法会读取静态字段flag的值并作为参数调用bar方法。
![](https://static001.geekbang.org/resource/image/c0/59/c024b8b45570f25534f76f0c4d378559.png)
**foo方法的IR图内联前**
在编译foo方法时其对应的IR图中将出现对bar方法的调用即上图中的5号Invoke节点。如果内联算法判定应当内联对bar方法的调用时那么即时编译器将开始解析bar方法的字节码并生成对应的IR图如下图所示。
![](https://static001.geekbang.org/resource/image/96/55/96d8575326f7c1991c6677e6d2d17155.png)
**bar方法的IR图**
接下来即时编译器便可以进行方法内联把bar方法所对应的IR图纳入到对foo方法的编译中。具体的操作便是将foo方法的IR图中5号Invoke节点替换为bar方法的IR图。
![](https://static001.geekbang.org/resource/image/62/c8/6209f233f5518ee470eb08422c8d0bc8.png)
**foo方法的IR图内联后**
除了将被调用方法的IR图节点复制到调用者方法的IR图中即时编译器还需额外完成下述三项操作。
第一被调用方法的传入参数节点将被替换为调用者方法进行方法调用时所传入参数对应的节点。在我们的例子中就是将bar方法IR图中的1号P(0)节点替换为foo方法IR图中的3号LoadField节点。
第二在调用者方法的IR图中所有指向原方法调用节点的数据依赖将重新指向被调用方法的返回节点。如果被调用方法存在多个返回节点则生成一个Phi节点将这些返回值聚合起来并作为原方法调用节点的替换对象。
在我们的例子中就是将8号==节点以及12号Return节点连接到原5号Invoke节点的边重新指向新生成的24号Phi节点中。
第三,如果被调用方法将抛出某种类型的异常,而调用者方法恰好有该异常类型的处理器,并且该异常处理器覆盖这一方法调用,那么即时编译器需要将被调用方法抛出异常的路径,与调用者方法的异常处理器相连接。
经过方法内联之后即时编译器将得到一个新的IR图并且在接下来的编译过程中对这个新的IR图进行进一步的优化。不过在上面这个例子中方法内联后的IR图并没有能够进一步优化的地方。
```
public final static boolean flag = true;
public final static int value0 = 0;
public final static int value1 = 1;
public static int foo(int value) {
int result = bar(flag);
if (result != 0) {
return result;
} else {
return value;
}
}
public static int bar(boolean flag) {
return flag ? value0 : value1;
}
```
不过如果我们将代码中的三个静态字段标记为final那么Java编译器注意不是即时编译器会将它们编译为常量值ConstantValue并且在字节码中直接使用这些常量值而非读取静态字段。举例来说bar方法对应的字节码如下所示。
```
public static int bar(boolean);
Code:
0: iload_0
1: ifeq 8
4: iconst_0
5: goto 9
8: iconst_1
9: ireturn
```
在编译foo方法时一旦即时编译器决定要内联对bar方法的调用那么它会将调用bar方法所使用的参数也就是常数1替换bar方法IR图中的参数。经过死代码消除之后bar方法将直接返回常数0所需复制的IR图也只有常数0这么一个节点。
经过方法内联之后foo方法的IR图将变成如下所示
![](https://static001.geekbang.org/resource/image/15/36/1506286ffb9c9d0d8a927e8174594536.png)
该IR图可以进一步优化死代码消除并最终得到这张极为简单的IR图
![](https://static001.geekbang.org/resource/image/6a/03/6affa54acd4d5f180efacdac93b02a03.png)
## 方法内联的条件
方法内联能够触发更多的优化。通常而言,内联越多,生成代码的执行效率越高。然而,对于即时编译器来说,内联越多,编译时间也就越长,而程序达到峰值性能的时刻也将被推迟。
此外内联越多也将导致生成的机器码越长。在Java虚拟机里编译生成的机器码会被部署到Code Cache之中。这个Code Cache是有大小限制的由Java虚拟机参数-XX:ReservedCodeCacheSize控制
这就意味着生成的机器码越长越容易填满Code Cache从而出现Code Cache已满即时编译已被关闭的警告信息CodeCache is full. Compiler has been disabled
因此即时编译器不会无限制地进行方法内联。下面我便列举即时编译器的部分内联规则。其他的特殊规则如自动拆箱总会被内联、Throwable类的方法不能被其他类中的方法所内联你可以直接参考[JDK的源代码](http://hg.openjdk.java.net/jdk/jdk/file/da387726a4f5/src/hotspot/share/opto/bytecodeInfo.cpp#l197)。)
**首先,由-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图的大小来决定方法调用能否被内联。
![](https://static001.geekbang.org/resource/image/49/c3/49fb3a3849e82ddcc74bd982a5e4eac3.jpg)
我在上面的表格列举了一些C2相关的虚拟机参数。总体来说即时编译器中的内联算法更青睐于小方法。
## 总结与实践
今天我介绍了方法内联的过程以及条件。
方法内联是指,在编译过程中,当遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。
即时编译器既可以在解析过程中替换方法调用字节码也可以在IR图中替换方法调用IR节点。这两者都需要将目标方法的参数以及返回值映射到当前方法来。
方法内联有许多规则。除了一些强制内联以及强制不内联的规则外即时编译器会根据方法调用的层数、方法调用指令所在的程序路径的热度、目标方法的调用次数及大小以及当前IR图的大小来决定方法调用能否被内联。
今天的实践环节,你可以利用虚拟机参数-XX:+PrintInlining来打印编译过程中的内联情况。具体每项内联信息所代表的意思你可以参考[这一网页](https://wiki.openjdk.java.net/display/HotSpot/Server+Compiler+Inlining+Messages)。