[讨论] Java与C/C++的性能对比

RednaxelaFX 2011-02-14
另外再给一个对比的例子。把前面的null检查例子再改造一下,来个第三版:
public class TestC2NullCheck3 {
  public static int getValue(Foo foo) {
    if (foo == null) throw new NullPointerException();
    return foo.value;
  }
  
  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 120000; i++) {
      getValue(new Foo());
    }
    Thread.sleep(2000);
  }
}

class Foo {
  public int value;
}

这个版本在Java源码里加入了显式的null检查。那么HotSpot的server模式编译器会如何处理呢?

这样跑:
java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand='compileonly,TestC2NullCheck3.getValue' -XX:-UseFastAccessorMethods -XX:+PrintAssembly TestC2NullCheck3


Decoding compiled method 0xb47eb408:
Code:
[Disassembling for mach='i386']
[Entry Point]
[Verified Entry Point]
  # {method} 'getValue' '(LFoo;)I' in 'TestC2NullCheck3'
  # parm0:    ecx       = 'Foo'
  #           [sp+0x20]  (sp of caller)
  0xb47eb500: mov    %eax,-0x4000(%esp)
  0xb47eb507: push   %ebp
  0xb47eb508: sub    $0x18,%esp         ;*synchronization entry
                                        ; - TestC2NullCheck3::getValue@-1 (line 3)
  0xb47eb50e: mov    0x8(%ecx),%eax     ;*getfield value
                                        ; - TestC2NullCheck3::getValue@13 (line 4)
                                        ; implicit exception: dispatches to 0xb47eb51c
  0xb47eb511: add    $0x18,%esp
  0xb47eb514: pop    %ebp
  0xb47eb515: test   %eax,0xb7734000    ;   {poll_return}
  0xb47eb51b: ret    
  0xb47eb51c: mov    %ecx,(%esp)
  0xb47eb51f: mov    $0xffffffad,%ecx
  0xb47eb524: nop    
  0xb47eb525: nop    
  0xb47eb526: nop    
  0xb47eb527: call   0xb47cf720         ; OopMap{[0]=Oop off=44}
                                        ;*ifnonnull
                                        ; - TestC2NullCheck3::getValue@1 (line 3)
                                        ;   {runtime_call}
  0xb47eb52c: call   0x014352a0         ;*ifnonnull
                                        ; - TestC2NullCheck3::getValue@1 (line 3)
                                        ;   {runtime_call}
  0xb47eb531: hlt    
  0xb47eb532: hlt    
  0xb47eb533: hlt    
  0xb47eb534: hlt    
  0xb47eb535: hlt    
  0xb47eb536: hlt    
  0xb47eb537: hlt    
  0xb47eb538: hlt    
  0xb47eb539: hlt    
  0xb47eb53a: hlt    
  0xb47eb53b: hlt    
  0xb47eb53c: hlt    
  0xb47eb53d: hlt    
  0xb47eb53e: hlt    
  0xb47eb53f: hlt    
[Exception Handler]
[Stub Code]
  0xb47eb540: jmp    0xb47e9ba0         ;   {no_reloc}
[Deopt Handler Code]
  0xb47eb545: push   $0xb47eb545        ;   {section_word}
  0xb47eb54a: jmp    0xb47d0c00         ;   {runtime_call}
[Constants]
  0xb47eb54f: int3

跟最初的版本比较一下看看?是不是基本上一样? 
IcyFenix 2011-02-14
撒迦你真是个勤奋的好孩子,不过2、3点才睡觉第二天能爬起来上班么?

看完你几个TestCase和分析,系统对隐式NPE处理的过程了解更深入了一个层次。三楼的帖子与你后面的分析方向、结论一致,深度却差了许多。顶楼的帖子就这样,不编辑了,后面的事情几句话也不好说清楚,有兴趣的同学看完全贴应也不会误导。
RednaxelaFX 2011-02-14
IcyFenix 写道
撒迦你真是个勤奋的好孩子,不过2、3点才睡觉第二天能爬起来上班么?

为啥不能…该什么时候上班就什么时候

那么换下一个话题,引用顶楼的另一段:
IcyFenix 写道
首先,因为JIT编译器运行占用的是用户程序运行时间,具有很大的时间压力,它能提供的优化手段也严重受制于编译成本。如果编译速度不能达到要求,那用户将在启动程序或程序的某部分察觉到重大延迟,这点使得JIT编译器不敢随便引入大规模的优化技术,而编译的时间成本在静态优化编译器中并不是主要的关注点。

“严重受制”倒真的不一定。有好几种处理的办法。

早期的JVM所使用的JIT编译器是同步的,工作流程是:
调用一个方法时看它有没有被编译过 -> 还没编译的话,触发编译,并一直等到编译完成为止 -> 直接跳进编译好的代码里执行,下次再调用该方法就不需要再检查了,直接进到编译好的代码里。这种工作方式是真正意义上的“just-in-time”,所以那些编译器才是准确意义上的JIT编译器(而像HotSpot里的动态编译器准确来说则不是“JIT”——它的工作时间往往比“just-in-time”还迟一些。
因为是同步的,所以不能耗太多时间,不然用户会很明显的感到程序“卡住了”。所以确实是受制于编译开销而不得不只做很少优化或者根本不做优化。那么做出来的就是“baseline JIT”。
现在.NET的CLR仍然是采用这种方式来实现JIT的…CLRv4没看到这方面的进步。
Oracle JRockit虽然是纯编译的但与CLR的方式比较不同,下面再提。

现在的高性能JVM不是这样做的。
Oracle HotSpot和IBM J9都是用混合模式执行(mixed-mode execution),有解释器和编译器。其中多数的编译都是异步发生的。简单来说,当某个编译单元被触发了编译时,JVM可以在编译代码的同时也继续运行用户的程序,等到编译完成后异步将编译结果“安装”到正常执行的路径上。这叫做后台编译或者异步编译。
对现在的HotSpot来说,异步编译意味着一个方法在等待编译出结果的同时可以先继续在解释器里跑。控制该行为的参数是BackgroundCompilation,默认是true。如果使用-Xbatch或-XX:-BackgroundCompilation参数来启动则会禁用后台编译。

后台编译为编译器争取到了更多的编译时间,使得一些原本比较“重”的优化也可以整合到动态编译器里了。HotSpot的server模式编译器就是一个“中等优化编译器”,生成的代码质量可以与GCC开着-O2相提并论。

现在连IE9里的JavaScript引擎——JScript9——都采用这种后台编译的方式了,这种设计已经是大路货了

这部分可以参考我的演示稿里的200-203页。

JRockit与J9都有更有趣的一面。
早期的JIT编译器很少使用运行时profile,也不区分被编译的方法的特性,最多就是判断一下代码里是否包含循环,之类的。这样,无论什么方法都以同样的优化程度去编译,也限制了编译器能做的优化——有些执行次数不多的方法用力优化了也没啥用,但编译时间却耗掉了。
而有一种技巧叫“多层编译”(tiered compilation),可以是同一个编译器带有多种优化程度,也可以是多个优化程度不同的编译器混在一起用。通过预先设置好的开销-收益模型,可以用不同的优化程度来编译不同的方法;也可以在发现某个已经被优化程度j编译过的方法更加热了之后,还可以用优化程度k(k >= j)来再编译一遍。
配合上后台编译,JRockit与J9都可以让一些热的代码一边在低优化程度上跑一边用高优化程度编译,渐渐的就把程序的整体性能按需求优化上去了。Apache Harmony、Jikes RVM等也有类似的设计。
Oracle HotSpot在JDK7里会默认开启-XX:+TieredCompilation选项,将原本的解释器、client编译器与server编译器混在一起使用。嗯事实上在刚出的JDK6u25的测试版里已经是默认开启的了。

JRockit虽然是纯编译的(这点与CLR一样),但它带有多层编译而CLR没有,于是在编译方面JRockit会比CLR占优一些。

Chrome里的JavaScript引擎——V8——最近也从单一优化程度的JIT编译进化为多层编译了。可以预见这种做法过不了多久也会成为大路货
IcyFenix 2011-02-14
“严重制约”的说法可能真的“严重”了一些,但是我觉得不能因为在后台线程执行编译任务,就把编译的时间成本的关注度下降到与静态编译器那种程度。

对于单CPU机器来说,多线程完全就是N个人抢一把椅子的游戏,后台编译虽然看起来前台程序仍然在执行,但速度肯定降下来了,也就是用一段长时间的慢速运行代替了短时间的完全停顿而已,就如在CPU资源紧张的机器中使用i-CMS代替CMS收集器那样的小把戏。当然,JIT主要是讨论C2编译,这时候扯单CPU是有点不合时宜。

即使对于多CPU的机器来说,无论使用后台异步编译还是同步编译,在很多情况下(人比椅子多的情况下)它们对程序吞吐量的影响是一致的,并不因为异步而“更快”,甚至有时候说来异步编译对吞吐量的影响还大一些,只有当有部分CPU资源空闲时异步编译才能获取一点收益。

“编译占用运行期时间”这点始终直接决定了如果编译耗费的时间更多,最终输出本地代码执行时要追回的编译成本也就越大,那JIT编译获得收益的临界点也更远。面对这种情况,那些编译耗时高,但不一定效果特别好的优化措施就成为彻底的鸡肋了,而在静态优化编译器中,这类鸡肋去尝一尝也无妨。
IcyFenix 2011-02-14
每次撒加打那么多字,我只打这么一小段总觉得很不好意思
RednaxelaFX 2011-02-14
现在越来越多的情况是核“太多了”而内存管理跟不上,使得单个JVM很难吃满一台机器的资源。
现在许多JVM的状况是,如果把堆开得很大,就可能要承受比较长的GC停顿,这就不得不把GC堆设得稍微小一点。但计算总是与数据相关的,没有那么大的存储空间就无法并发的做那么大量的运算,所以核的个数在现在的一些服务器上反而有多的。大家还恨不得多想办法让这些核有事干呢…呃呵呵。

IcyFenix 写道
“严重制约”的说法可能真的“严重”了一些,但是我觉得不能因为在后台线程执行编译任务,就把编译的时间成本的关注度下降到与静态编译器那种程度。

当然没下降到跟静态编译器同等的程序,但编译的优化技巧是有收益考量的,事实上很多非常非常耗时的优化算法带来的额外收益很小,使得大家实际用的时候宁可用比较不那么精确的、开销较低但已经能达到理想效果的算法。

这里要深入讨论就必须用数字来说话了。不上数字的话就打住然后换下一个话题吧。

前面bugmenot说的对,到开发程序所消耗的开发时间,与运行程序能达到的运行效率,这两点要一起看来比较才有意义。不然C或者C++程序员总有办法多榨出一些性能出来,即便是付出很高的、不切实际的代价。
以前在.NET圈子里有过这么一系列帖子,相当有趣,值得读读看:http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx

另外Java也有比较神奇的实现,例如Excelsor JET,它不但像一般的C/C++编译器一样可以AOT(ahead-of-time)编译,同时也拥有一个动态编译器,使得它既能负担得起各种强力的耗时优化又能保持Java动态加载的语义。嗯唯一的缺点大概就是…它不免费
IcyFenix 2011-02-14
嗯,上数字的话,要找全有代表性和能模拟大多数情况的TestCase难度很大,测试过程也不确定,编译花销在多长时间后能在代码执行中追回来这些都不太好统计,这个话题我们就此打住换一个吧~~
RednaxelaFX 2011-02-14
那么先换个我能少写点字的话题。

IcyFenix 写道
Java语言中的对象内存分配都是堆上进行,只有方法中的局部变量才在栈上分配。而C/C++的对象则有多种内存分配方式,既可能在堆上分配,也可能在栈上分配,如果可以把线程私有的对象在栈上分配,将可以减轻内存回收的压力,也不需要考虑内存屏障方面的问题。

先参考HotSpot 17.0-b12的逃逸分析/标量替换的一个演示这个看看?
wkoffee 2011-02-14
我也来为java说两句,java对于c/c++来说还有一个优势就是类型安全,这对别名分析(alias analysis)带来很大的好处。c语言中的数据类型可以任意转换,对于compiler来说就是一场噩梦。例如一个简单的例子
void foo( ClassA a, ClassB b ){
  x=a.f1;
  b.f2=0;
  y=a.f1;
...

假设ClassA和ClassB没有继承关系,这里java的compiler可以安全的认为a和b指向的是不同的内存,y可以直接使用x的结果,或者reorder前两句的顺序都可以由compiler根据需要决定。如果换成c语言中的指针,虽然在绝大部分情况下这两块内存不重叠,但compiler必须采用最保守的策略。

还有lz说的inline,这个对优化的重要性无可比拟,我认为java在这方面也有优势。c compiler一般以一个c文件作为一个编译单元,只能在自己可见的范围内进行inline,所以绝大部分被inline的函数需要放在头文件中,这其实要很大程度上要依赖程序员的经验。java的jit compiler可以根据实际的运行情况决定如何inline,这个不需要程序员介入而且实际的效果更好。
wkoffee 2011-02-14
对于implicit null check(INC),还有些隐含的性能损失,它导入了一些新的control dependent,而且sun的c2对于含有INC的basic block有一些特殊的约定(具体为什么不清楚),我们的jit compiler下一步打算去掉这个优化,恢复explicit null check,使用其他的技术来达到它的效果。
Global site tag (gtag.js) - Google Analytics