JIT编译优化之方法内联

2020-11-02  本文已影响0人  花醉霜寒

如何实现方法调用

要如何实现方法调用呢?最直接的方法就是可以把调用的函数指令,直接插入在调用函数的地方,然后在编译器编译代码的时候,直接就把函数调用变成对应的指令替换掉,但是如果函数 A 调用了函数 B,然后函数 B 再调用函数 A,我们就得面临在 A 里面插入 B 的指令,然后在 B 里面插入 A 的指令,这样就会产生无穷无尽地替换。这样就像将两面镜子面对面放着,可以看到无穷无尽的镜子一样。如何解决这个问题呢?内存里面开辟一段空间,用栈这个后进先出(LIFO,Last In First Out)的数据结构。栈就像一个乒乓球桶,每次程序调用函数之前,我们都把调用返回后的地址写在一个乒乓球上,然后塞进这个球桶。这个操作其实就是我们常说的压栈。如果函数执行完了,我们就从球桶里取出最上面的那个乒乓球,很显然,这就是出栈。

int static add(int a, int b){ 
   return a+b;
}
int main(){ 
   int x = 5; 
   int y = 10; 
   int u = add(x, y);
}

编译后

int static add(int a, int b){ 
   0: 55 push rbp 
   1: 48 89 e5 mov rbp,rsp 
   4: 89 7d fc mov DWORD PTR [rbp-0x4],edi 
   7: 89 75 f8 mov DWORD PTR [rbp-0x8],esi 
   return a+b; 
   a: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 
   d: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 
   10: 01 d0 add eax,edx} 
   12: 5d pop rbp 
   13: c3 ret 0000000000000014 <main>:
int main(){ 
   14: 55 push rbp 
   15: 48 89 e5 mov rbp,rsp 
   18: 48 83 ec 10 sub rsp,0x10 
   int x = 5; 
   1c: c7 45 fc 05 00 00 00 mov DWORD PTR [rbp-0x4],0x5 
   int y = 10; 
   23: c7 45 f8 0a 00 00 00 mov DWORD PTR [rbp-0x8],0xa 
   int u = add(x, y); 
   2a: 8b 55 f8 mov edx,DWORD PTR [rbp-0x8] 
   2d: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 
   30: 89 d6 mov esi,edx 
   32: 89 c7 mov edi,eax 
   34: e8 c7 ff ff ff 
   call 0 
   39: 89 45 f4 mov DWORD PTR [rbp-0xc],eax 
   3c: b8 00 00 00 00 mov eax,0x0
} 
   41: c9 leave 
   42: c3 ret

首先明白两个概念:

call指令相当于 push rip 和 jmp 的结合,rip 是指令地址寄存器,也就是将当前执行函数PC寄存器中的指令地址入栈,并跳转到相应函数处执行,而 ret 指令相当于 pop rip 和 jmp指令的结合,
rbp 和 rsp 用于维护当前帧栈,rbp 指向栈帧的栈底地址,rsp 指向栈顶地址,push pop 和 mov rbp,rsp 主要是为了从卖你函数的栈帧调整为add函数的栈帧。
具体调用过程如下所示


方法调用指令执行过程

方法内联

上文中提到方法调用最简单的方法就是把被调用函数的指令,直接插入在调用函数的地方,虽然这个方法在一些调用关系比较复杂的场景中存在问题,但是对于调用关系比较简单,如上文中add方法,在add方法没有调用其他方法,那么直接将add方法的指令插入main方法中是可行的,这就是很多编译器进行优化的重要场景之一。JVM中大名鼎鼎的JIT便提供这种优化能力。
上文中的例子如果在编译过程中加上优化编译的参数

$ gcc -g -c -O function_example_inline.c
$ objdump -d -M intel -S function_example_inline.o

那么在main函数调用add方法时不会被编译为call指令,而是直接调用add指令,即将add方法的指令插入了main方法中。
方法内联会给我们带来哪些收益呢?

总的来说就是方法执行的效率提高了,但是方法内联也是有利有弊的,那么方法内联会带来哪些问题呢?

权衡方法内联的利弊,可以总结一下最佳实践

-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来
-XX:CompileThreshold //参数设置识别为热点方法的阈值
-XX:MaxFreqInlineSize=N //如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联
-XX:MaxInlineSize=N //如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联

参考资料

java程序猿-你的代码居然慢在JIT方法内联上
极客时间-《深入浅出计算机组成原理之函数调用:为什么会发生stack overflow?》

上一篇 下一篇

猜你喜欢

热点阅读