inline 方法内联
什么是方法内联?
- 如果你是C/C++ 程序员,你可能十分了解,C/C++ 使用inline关键字,它用作编译器指令,建议(但不要求)编译器通过执行内联扩展(即在每个函数调用的地址处插入函数代码)来内联替换函数的主体,从而节省函数调用的开销。
- 如果你是Java 码农,你在背八股时可能听过,JVM在进行JIT优化时会使用到方法内联进行优化
- 如果你是goer,在看go sdk 时会看到
//go:noinline
,提示编译器不要进行方法内联
所以方法内联到底是什么呢?
方法内联就是:将函数调用调用处替换为被调用函数主体的一种编译器优化手段
举个例子:
C
void func(){
// doSomeThing...
}
int main(){
while(true){
func(); // 函数调用,涉及额外的开销
}
}
//---After inline
int main(){
while(true){
// doSomeThing... // 函数体直接复制到这里,减少了函数调用的开销
}
}
JIT 方法内联
作为Java程序员我们是决定不了哪个方法需要内联的,但是我们可以看到JIT对方法进行了内联操作
打开JVM 参数:
ruby
-XX:+PrintCompilation //记录 JIT 编译发生的时间
-XX:+UnlockDiagnosticVMOptions //启用其他标志
-XX:+PrintInlining //打印要内联的方法
运行示例代码如下:
Java
package org.example.inline;
public class Main {
private static class Sum {
private final int n;
public Sum(int n) {
this.n = n;
}
public long getTotalSum() {
long totalSum = 0;
for (int i = 0; i < n; i++) {
totalSum += i;
}
return totalSum;
}
}
private static long calculateSum(int n) {
return new Sum(n).getTotalSum();
}
private static final long NUMBERS_OF_ITERATIONS = 15000;
public static void main(String[] args) {
for (int i = 1; i < NUMBERS_OF_ITERATIONS; i++) {
calculateSum(i);
}
}
}
我们在控制台可以看到非常多方法被内联了,我们找到我们关心的方法calculateSum
python
288 395 4 org.example.inline.Main::calculateSum (12 bytes)
290 384 3 org.example.inline.Main::calculateSum (12 bytes) made not entrant
@ 5 org.example.inline.Main$Sum::<init> (10 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 8 org.example.inline.Main$Sum::getTotalSum (25 bytes) inline (hot)
- 表示Main$Sum类的构造函数在字节码位置5处被内联,因为它是一个"hot"方法,即被频繁调用的方法。
- 表示java.lang.Object类的构造函数被内联,因为它也是一个"hot"方法。
- 表示getTotalSum方法在字节码位置8处被内联,原因同样是因为它是一个"hot"方法。
当方法被调用超过特定次数时,它会变为 "hot"。默认情况下,此阈值设置为 10,000
我们绝对不想内联所有内容,因为这会很耗时并且会产生一个巨大的字节码。
方法内联的优缺点
优势:
- 减少函数调用的开销,提高执行速度。
- 复制后的更大函数体为其他编译优化带来可能性,如 过程间优化
- 消除分支,并改善空间局部性和指令顺序性,同样可以提高性能。
问题:
- 代码复制带来的空间增长。
- 如果有大量重复代码,反而会降低缓存命中率,尤其对 CPU 缓存是致命的。