本来这篇文章还有一个小标题,完整的大概是这样:“JIT优化——如何让解释型语言的执行效率超过编译型语言”。但是这太夸张了,已经算是标题党了,所以我把这个小标题砍掉了。
这还是一篇为上课分享而写的文章,所以照例先写一些废话。之前老师上课讲到解释型和编译型语言谁的效率更高的时候,给出了一个非常绝对的结论,肯定是编译型的语言效率最高,因为解释型的语言需要边解释边执行,造成的性能开销肯定更大,效率更差。
照理来说是这样的,而且统计数据也证实了这一点。有开发者根据 The Benchmarks Game 的测试数据制作了一份可视化图表,具体可见:编程语言效率可视化
事实上从绝对性能上来看,Go,Java一类的解释型语言效率并不差,但肯定是没办法跟C相提并论的。
Sun在上个世纪就已经意识到了这个问题,当然也给出了一些解决方案,其中之一便是JIT。先不讲原理,看效果。
//C版本 #include <stdio.h> #include <time.h> int f(int n) { return (n<3)? 1 : f(n-1)+f(n-2);//如果是前两项,则输出1 } int main(int argc, const char * argv[]) { int n; clock_t start, finish; double Total_time; n=45; start = clock(); printf("%d\n",f(n)); finish = clock(); Total_time = (double)(finish - start) / CLOCKS_PER_SEC; //单位换算成秒 printf("%f seconds\n", Total_time); return 0; }
//Java版本 public class Main { public static int f(int n){ return (n<3)? 1 : f(n-1)+f(n-2);//如果是前两项,则输出1 } public static void main(String[] args) { int n; long start, finish; double Total_time; n=45; start = System.currentTimeMillis(); System.out.println(f(n)); finish = System.currentTimeMillis(); Total_time = (double)(finish - start) / 1000; //单位换算成秒 System.out.println(Total_time); } }
# Python 版本 import time def f(n): if n < 3: return 1 else: return f(n - 1) + f(n - 2) if __name__ == "__main__": n = 45 start = time.time() print(f(45)) end = time.time() print(end - start)
很简单,就是计算斐波那契数列的值,这里初始值取45,能看出性能的差距,耗费的时间也不会很长(Python除外)。
运行时间如下:
//C 1134903170 3.865728 seconds Program ended with exit code: 0 //Java 1134903170 2.515 进程已结束,退出代码0 //Python 1134903170 207.55029606819153 进程已结束,退出代码0
可以看到一个奇怪的现象,那就是Java运行耗时会比C更短。在代码结构完全相同的情况下,JVM一定在运行时帮我们做了很多工作。在这段代码中,功劳最大的便是JIT(其实是Java的内存管理)。
JIT即时编译器介绍
为了优化Java的性能 ,JVM在解释器之外引入了即时(Just In Time)编译器:当程序运行时,解释器首先发挥作用,代码可以直接执行。随着时间推移,即时编译器逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。
即时编译器极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。目前,即时编译器已经非常成熟了,在性能层面甚至可以和编译型语言相比。不过在这个领域,大家依然在不断探索如何结合不同的编译方式,使用更加智能的手段来提升程序的运行速度。
上面这段话又是我抄过来的。简而言之就是,JVM会将一些经常执行的代码段编译为机器码来执行,这部分代码的执行效率便可以与编译型语言相媲美。很显然,上述斐波那契数列递归计算部分一定被编译为机器码。
但这不足以解释为什么Java的执行效率会超过C。JVM一定还做了其他工作!
接下来我通过一个例子来说明JVM到底做了哪些工作,当然很不全面,例子也是我抄来的。
首先给出一段代码:
class Calculator { Wrapper wrapper; public void calculate() { y = wrapper.get(); z = wrapper.get(); sum = y + z; } } class Wrapper { final int value; final int get() { return value; } }
假设这段代码是Hot Spot,JVM并不会直接把这段代码编译为机器码,而是会对其进行优化。最终是把下面这段代码编译为机器码。
class Calculator { Wrapper wrapper; public void calculate() { y = wrapper.value; sum = y + y; } } class Wrapper { final int value; final int get() { return value; } }
这些代码的优化都是遵循一些固定的方法的,一步一步进行优化。
- Inline Method(方法内联)
用b.value
取代wrapper.get()
, 不透过函数呼叫而直接存取wrapper.value
来减少延迟。
class Calculator { Wrapper wrapper; public void calculate() { y = wrapper.value; z = wrapper.value; sum = y + z; } }
- 移除多余的载入
用z = y
取代z = wrapper.value
, 所以只存取区域变量而不是wrapper.value
来减少延迟。
class Calculator { Wrapper wrapper; public void calculate() { y = wrapper.value; z = y; sum = y + z; } }
- Copy Propagation(复写传播)
用y = y
取代z = y
,没有必要再用一个变量z
,因为z
跟y
会是相等的。
class Calculator { Wrapper wrapper; public void calculate() { y = wrapper.value; y = y; sum = y + y; } }
- 消除不用的源代码
y = y
是不必要的,可以消灭掉。
class Calculator { Wrapper wrapper; public void calculate() { y = wrapper.value; sum = y + y; } }
最终优化完成。
一些小提示,提示我上课的时候要讲什么:
JMH基准测试
jit好智能啊,太厉害了
其实有的时候优化方式很激进,代码改的出问题了,还会自动回退
对了,Java计算代码与代码间的执行时间使用System.nanoTime()更加精确