zoukankan      html  css  js  c++  java
  • 为什么 JVM 不用 JIT 全程编译

    从知乎扣出来的内容 https://www.zhihu.com/question/37389356

    作者:RednaxelaFX
    链接:https://www.zhihu.com/question/37389356/answer/73820511
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。

    1. 编译的时间开销

    解释器的执行,抽象的看是这样的:
    输入的代码 -> [ 解释器 解释执行 ] -> 执行结果
    
    而要JIT编译然后再执行的话,抽象的看则是:
    输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果
    

    说JIT比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释”这个动作快。

    然而这JIT编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。

    所以,对“只执行一次”的代码而言,解释执行其实总是比JIT编译执行要快。
    怎么算是“只执行一次的代码”呢?粗略说,下面两个条件同时满足时就是严格的“只执行一次”
    • 只被调用一次,例如类的初始化器(class initializer,<clinit>()V)
    • 没有循环

    对只执行一次的代码做JIT编译再执行,可以说是得不偿失。
    对只执行少量次数的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。
    只有对频繁执行的代码,JIT编译才能保证有正面的收益。

    况且,并不是说JIT编译了的代码就一定会比解释执行快。切不可盲目认为有了JIT就可以鄙视解释器了,还是得看实现细节如何。
    有个很经典的例子:LuaJIT 2里有一个实现得非常优化的解释器,它解释执行的速度甚至比LuaJIT 1的JIT编译后的代码的速度还要快。

    2. 编译的空间开销

    举个最简单的例子:
      public static int foo() {
        return 42;
      }
    
    其字节码大小只有3字节:
      public static int foo();
        Code:
          stack=1, locals=0, args_size=0
             0: bipush        42
             2: ireturn
    
    而由Linux/x86-64上的HotSpot VM的Server Compiler将其编译为机器码后,则膨胀到了56字节:
      # {method} 'foo' '()I' in 'XX'
      #           [sp+0x20]  (sp of caller)
      0x00000001017b8200: sub    $0x18,%rsp
      0x00000001017b8207: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                                    ; - XX::foo@-1 (line 3)
      0x00000001017b820c: mov    $0x2a,%eax
      0x00000001017b8211: add    $0x10,%rsp
      0x00000001017b8215: pop    %rbp
      0x00000001017b8216: test   %eax,-0x146021c(%rip)        # 0x0000000100358000
                                                    ;   {poll_return}
      0x00000001017b821c: retq   
      0x00000001017b821d: hlt    
      0x00000001017b821e: hlt    
      0x00000001017b821f: hlt    
    [Exception Handler]
    [Stub Code]
      0x00000001017b8220: jmpq   0x00000001017b50a0  ;   {no_reloc}
    [Deopt Handler Code]
      0x00000001017b8225: callq  0x00000001017b822a
      0x00000001017b822a: subq   $0x5,(%rsp)
      0x00000001017b822f: jmpq   0x000000010178eb00  ;   {runtime_call}
      0x00000001017b8234: hlt    
      0x00000001017b8235: hlt    
      0x00000001017b8236: hlt    
      0x00000001017b8237: hlt    
    

    对一般的Java方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到10x是很正常的。上面的例子比较极端一些,但还是很能反映现实状况的。

    同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致“代码爆炸”(code size explosion)。

    3. 编译时机对优化的影响

    有些JIT编译器非常简单,基本上不做啥优化,也倒也没啥影响。
    但现代做优化的JIT编译器都非常注重使用profile信息,而profile是需要通过执行用户程序来获取的。
    这样,编译得太早的话,就来不及收集足够profile信息,进而会影响优化的效果;而编译太迟的话,即便收集了很多高质量的profile,但却也已经付出了profile的额外开销,编译出来的代码再快或许也弥补不过来了。

    在解释器里实现收集profile的功能,等解释执行一段时间后再触发JIT编译,这样就可以很好的平衡收集profile与编译优化这两方面。

    当然,收集profile也可以在JIT编译器里做:一开始先JIT编译生成收集profile的版本的代码,等收集了到足够profile后触发重新编译,再生成出优化的、不带profile的版本。JRockit基本上就是这样做的。这方面在本回答开头放的链接里已有说明。
  • 相关阅读:
    DSP Builder设计一个滤波器
    Modelsim 10.0 对Altera FFT IP 进行仿真
    FPGA内部计算小数
    TIOBE 2012年3月编程语言排行榜:JS超越Perl和Python
    转载 10个新鲜的Ajax相关的jQuery插件
    转载 使用HTML5、CSS3和jQuery增强网站用户体验
    转载 Java堆内存的10个要点
    累 腾讯笔试
    python 浮点数取整
    转载 一个页面重构工程师眼中的“用户体验”
  • 原文地址:https://www.cnblogs.com/devilwind/p/7374082.html
Copyright © 2011-2022 走看看