20 | 方法内联(上)
下载APP
关闭
渠道合作
推荐作者
20 | 方法内联(上)
2018-09-05 郑雨迪 来自北京
《深入拆解Java虚拟机》
课程介绍
讲述:郑雨迪
时长07:40大小3.52M
在前面的篇章中,我多次提到了方法内联这项技术。它指的是:在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。
方法内联不仅可以消除调用本身带来的性能开销,还可以进一步触发更多的优化。因此,它可以算是编译优化里最为重要的一环。
以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作仅剩字段访问。
在 C2 中,方法内联是在解析字节码的过程中完成的。每当碰到方法调用字节码时,C2 将决定是否需要内联该方法调用。如果需要内联,则开始解析目标方法的字节码。
复习一下:即时编译器首先解析字节码,并生成 IR 图,然后在该 IR 图上进行优化。优化是由一个个独立的优化阶段(optimization phase)串联起来的。每个优化阶段都会对 IR 图进行转换。最后即时编译器根据 IR 图的节点以及调度顺序生成机器码。
同 C2 一样,Graal 也会在解析字节码的过程中进行方法调用的内联。此外,Graal 还拥有一个独立的优化阶段,来寻找指代方法调用的 IR 节点,并将之替换为目标方法的 IR 图。这个过程相对来说比较形象一些,因此,今天我就利用它来给你讲解一下方法内联。
上面这段代码中的 foo 方法将接收一个 int 类型的参数,而 bar 方法将接收一个 boolean 类型的参数。其中,foo 方法会读取静态字段 flag 的值,并作为参数调用 bar 方法。
foo 方法的 IR 图(内联前)
在编译 foo 方法时,其对应的 IR 图中将出现对 bar 方法的调用,即上图中的 5 号 Invoke 节点。如果内联算法判定应当内联对 bar 方法的调用时,那么即时编译器将开始解析 bar 方法的字节码,并生成对应的 IR 图,如下图所示。
bar 方法的 IR 图
接下来,即时编译器便可以进行方法内联,把 bar 方法所对应的 IR 图纳入到对 foo 方法的编译中。具体的操作便是将 foo 方法的 IR 图中 5 号 Invoke 节点替换为 bar 方法的 IR 图。
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 图并没有能够进一步优化的地方。
不过,如果我们将代码中的三个静态字段标记为 final,那么 Java 编译器(注意不是即时编译器)会将它们编译为常量值(ConstantValue),并且在字节码中直接使用这些常量值,而非读取静态字段。举例来说,bar 方法对应的字节码如下所示。
在编译 foo 方法时,一旦即时编译器决定要内联对 bar 方法的调用,那么它会将调用 bar 方法所使用的参数,也就是常数 1,替换 bar 方法 IR 图中的参数。经过死代码消除之后,bar 方法将直接返回常数 0,所需复制的 IR 图也只有常数 0 这么一个节点。
经过方法内联之后,foo 方法的 IR 图将变成如下所示:
该 IR 图可以进一步优化(死代码消除),并最终得到这张极为简单的 IR 图:
方法内联的条件
方法内联能够触发更多的优化。通常而言,内联越多,生成代码的执行效率越高。然而,对于即时编译器来说,内联越多,编译时间也就越长,而程序达到峰值性能的时刻也将被推迟。
此外,内联越多也将导致生成的机器码越长。在 Java 虚拟机里,编译生成的机器码会被部署到 Code Cache 之中。这个 Code Cache 是有大小限制的(由 Java 虚拟机参数 -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 图的大小来决定方法调用能否被内联。
我在上面的表格列举了一些 C2 相关的虚拟机参数。总体来说,即时编译器中的内联算法更青睐于小方法。
总结与实践
今天我介绍了方法内联的过程以及条件。
方法内联是指,在编译过程中,当遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。
即时编译器既可以在解析过程中替换方法调用字节码,也可以在 IR 图中替换方法调用 IR 节点。这两者都需要将目标方法的参数以及返回值映射到当前方法来。
方法内联有许多规则。除了一些强制内联以及强制不内联的规则外,即时编译器会根据方法调用的层数、方法调用指令所在的程序路径的热度、目标方法的调用次数及大小,以及当前 IR 图的大小来决定方法调用能否被内联。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 13
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
19 | Java字节码(基础篇)
下一篇
21 | 方法内联(下)
精选留言(24)
- 木心2018-09-11IR只有我看不懂吗?各颜色的模块代表什么意思,还有不同钥匙的线代表什么意思?共 5 条评论55
- 钱2018-09-10方法内联,一种优化代码的手段,其目的就是想让代码执行的更快一些,它怎么做到的呢?以前记录过性能优化的思路就那么几种,让赶的快的干,如果实现不了就让干的快的多干,干的慢点少干。方法内联是采用少干活的方式来提高效率的,直接将对应方法的字节码内联过来,省下了记录切换上下文环境的时间和空间。16
- Jerry银银2019-12-29将Java程序编译字节码的时候,Java编译器会有方法内联的优化吗?
作者回复: javac几乎不做任何优化
12 - 饭粒2019-12-24这个和 C++ 内联函数类似吧,目的是减少函数调用的开销。最终都是编译器来优化,C++ 通过 inline 声明函数,建议编译器内联编译。Java 是 JVM 自动处理,也可通过 VM 参数控制。
作者回复: 对的
共 2 条评论7 - 刘冠利2018-09-06请问final的使用对内联有多大帮助?
作者回复: 在(下)篇有介绍
6 - 西门吹牛2020-07-20内联是一种编译器的优化手段,目的就是让代码执行更快,把频繁调用的方法,进行内联后,把调用的目标方法直接编译成机器代码,减少目标方法频繁调用的开销,如果不内联,程序需要保存当前调用者方法的执行位置,同时还要创建用于调用目标方法的栈帧,目标方法执行结束,还是再恢复调用者方法的执行,开销很大。内联的实现过程有俩种: 第一,在即时编译的过程中,可以根据一定的规则,将目标方法的方法体直接编译为机器码; 第二,在IR 图中替换目标方法调动IR 节点,Java字节码本身作为一种 IR,不可直接优化,所以即时编译器会将字节码转换成可优化的IR,IR可以理解为一种字节码指令在虚拟机中运行的分支流程和数据流程图,IR 图中的每个节点可以看出是程序执行的一个或多个指令,把调用目标方法的IR 节点,替换成目标方法的IR 图,其实就是把俩个方法的IR 图合并,这样可以对合并后的 IR 图进行优化; 无论是哪种内联过程,本质是将俩个方法合并,也就是把目标方法合并到调用方法里面,合并方法之后,还需要将目标方法的参数和返回值,都映射到调用方的方法里面。展开4
- 李二木2018-09-07请问方法内联是发生在解释执行阶段吗?这里方法调用可以理解为解释执行中的小部分解释吗?有些困惑,麻烦老师解释执行下。
作者回复: 方法内联只发生在即时编译器中。 方法调用就是字面意思。在即时编译过程中,即时编译器会将当前方法所包含的方法调用的目标方法纳入编译范围中。
4 - Monday2020-07-15硬着头皮看完了,一起理解方法内联就是,把bar的方法代码在foo中展开,内联后代码如下: public static boolean flag = true; public static int value0 = 0; public static int value1 = 1; public static int foo(int value) { int result = flag ? value0 : value1;; if (result != 0) { return result; } else { return value; } }展开3
- Geek_9871692018-10-21老师,能否提供一个学习IR图的地址?
作者回复: 这方面的知识网上并不多。可以知乎上搜Sea of nodes IR,看R大的回答,有不少链接可以参考。
3 - 皮卡皮卡丘2018-09-05这个是方法内联信息吗,怎么和代码里的信息有差别?@ 1 java.lang.Object::<init> (1 bytes) @ 5 java.lang.AbstractStringBuilder::appendNull (56 bytes) callee is too large @ 10 java.lang.String::length (6 bytes) @ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) @ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) callee is too large @ 20 java.util.Arrays::copyOf (19 bytes) @ 11 java.lang.Math::min (11 bytes) @ 14 java.lang.System::arraycopy (0 bytes) intrinsic @ 35 java.lang.String::getChars (62 bytes) callee is too large @ 1 java.lang.Object::<init> (1 bytes) @ 13 java/lang/StringIndexOutOfBoundsException::<init> (not loaded) not inlineable @ 30 java/lang/StringIndexOutOfBoundsException::<init> (not loaded) not inlineable @ 65 java/lang/StringIndexOutOfBoundsException::<init> (not loaded) not inlineable @ 75 java.util.Arrays::copyOfRange (63 bytes) callee is too large @ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) callee is too large @ 20 java.util.Arrays::copyOf (19 bytes) @ 11 java.lang.Math::min (11 bytes) @ 14 java.lang.System::arraycopy (0 bytes) intrinsic @ 66 java.lang.String::indexOfSupplementary (71 bytes) callee is too large @ 3 java.lang.String::indexOf (70 bytes) callee is too large @ 1 java.lang.Character::toUpperCase (9 bytes)展开
作者回复: PrintInlining将打印JVM里所有即时编译的内联优化信息,所以看起来比较杂
共 2 条评论2 - 叫啥不行2020-06-17乱糟糟的,前一秒说方法内联后的 IR 图并没有能够进一步优化的地方。后面就提了final字节码会取常量,这个算优化么?在下一步又说foo可以死代码消除,这是因为final还是跟final没啥关系,只是跳跃式讲解,不是说没有进一步优化的地方了么共 1 条评论1
- MZC2019-12-26IR图 不知道是干什么的 老师 而且 您的专栏里边 好多名词都不太懂 还望老师抽时间回答一下1
- 随心而至2019-10-25IR的图我也没看懂,不过内联想要做的事情看明白了,感觉和C/C++里面的define有点像1
- gogo2019-09-30老师,是只有即时编译才会进行方法内联吗?jdk编译java源码的时候会进行方法内联吗1
- 乘风2019-07-18感谢雨迪,之前对方法内联疑惑很多,知道有方法内联这件非常优秀的优化技术,却不知道如何在一定范围内优化代码来提高方法内联的几率。1
- 满大大2023-01-09 来自浙江方法内联后 调用堆栈里 还有它么
- Geek_d00eb12022-08-26 来自北京“其次,如果调用字节码对应的符号引用未被解析、目标方法所在的类未被初始化” 老师,这段话的意思是不是指的前几章讲的invokeDynamic类型的方法调用呀?
- 啸疯2021-12-29结合操作系统的一些细节,试着理解下: 方法调用会伴随着当前线程方法区的入栈和出栈,CPU寄存器中控制寄存器也需要记录指令的前后地址。而栈的出入和寄存器的地址切换都是需要时间的,虽然时间很短,但是对比到CPU的频率来说,这个时间就比较长了,所以通过方法内联,可以省去这部分的时间,提高执行效率。 而虚拟机进行内联的方式,要么是直接替换为被调用方法的字节码,要么是在IR中替换节点(因为IR最终也会被编译成机器码)。 至于IR图,推荐https://darksi.de/d.sea-of-nodes/这篇文章展开
- nuclear2021-04-23scala中有@inline注解,是指scalac在编译生成字节码的过程中做内联吗
- 小陈2020-03-29这个图不错哈