西西河

主题:【原创】Java和.NET哪个运行的更快? -- Highway

共:💬24 🌺19 新:
分页树展主题 · 全看 下页
  • 家园 【原创】Java和.NET哪个运行的更快?

    对于这个问题,有的人回答得非常干脆:“当然是.NET了”。你如果问他为什么,他会简单的回答:“.NET有JIT(Just-in-time-compiler),运行的是编译好的机器代码,Java是解释执行的,速度当然不能和.NET的Native code相提并论了!”

    事实是这样的吗?

    Java和.NET到底哪个运行的更快可不是一个简单的问题,你千万不要相信任何一个简单的Benchmark程序的测试结果。因为性能问题涉及的方面太广,很难简单武断的下结论的。比如说要全面比较Java和.NET的性能,你可能要测试整数,浮点数,各种String操作,Collection(standard and generics)性能, XML (SAX and DOM) 操作,文件IO (读,写,顺序,随机,文本,二进制),数据库操作,线程同步(Thread synchronization), Lock机制,网络操作,串行化/反串行化,Memory allocate/release, Garbage collection等等等等。就我做过的一些测试来看,Java和.NET各有千秋,各有擅长的地方,也都有自己的弱点。

    今天我在这里想说的是Java和.NET在执行程序时各自的特点,希望你看后能对Java和.NET有个更清晰地认识(或是给我扔几块砖,让我有个更清晰地认识)。

    Java的Hotspot引擎

    性能是早期Java的一个最大问题。为了这事,SUN和业界的其他公司没少下功夫,也曾经出现了很多方案和提议。比较有趣的建议如开发专门执行Java的协处理器;将Java byte code转化为本地代码;甚至是将Java源程序先转化为C++,然后编译成C++可执行文件等等。那时候,不少公司都开发了自己的Java虚拟机,比如IBM,微软。到后来,随着Java的不断发展和成熟,最后SUN的Hotspot引擎脱颖而出了,成了最广泛使用的Java虚拟机。

    点看全图

    外链图片需谨慎,可能会被源头改

    Hotspot Engine到底是如何工作的呢?

    当Java虚拟机拿到byte code的时候,它总是先要解释执行。与此同时,它观察和记录程序的执行特点。随着程序的不断运行,它对程序的行为越来越清楚,它会发现程序中的热点(就是所谓的Hotspot)。这些热点就是频繁被执行,占用CPU最多的程序段。这时候,它就会编译这段程序,将它转化为本地代码(Native code),,由于它在这个时候掌握了程序的执行特点,所以它敢于“大胆”地进行程序优化,比如说最常见的Method inlining。这样一来,瓶颈被突破了,程序运行速度立刻得到提升,在很多类型的运算上,其速度和C++相似,甚至更快。这就是所谓的“动态优化”。

    有人说了,Java code怎么可能比C++更快呢?原因很简单,C++进行的优化是静态优化,都是在编译的时候进行的。一旦编译链接生成的可执行本地代码,就盖棺定论了,不能更改了,除非是Hacker或是病毒。就现在的编译技术来看,静态优化在总体上还是最成熟的,并且在编译的时候它没有时间压力,可以花很长时间来优化程序。这点Java和.NET是不允许的。但是静态优化也有它的缺点,因为它不知道这些程序在运行的时候具体会有什么特征,无法针对性地进行优化。比如它就不可能“大胆”的进行Method inlining。因为它胆子大了就可能犯错误。比如一个Class A,它有个简单函数public int Foo() {return 3;},它的两个子类B和C Override了这个Foo()函数。那么在静态编译的时候,C++的编译器不能将Foo()这个函数作inlining优化,因为它不知道在运行的时候到底是A,还是B或是C的Foo()被调用。而Java的虚拟机在运行时就会知道这些信息。如果发现运行的时候是B的Foo()在反复被调用,那么它就大胆的将B的Foo()拿到调用者的程序里面来,这样的inlining处理避免了Function call的开销(仔细说就是No method call;No dynamic dispatch;Possible to constant-fold the value)。对于简单的小函数,调用开销往往比执行还费时间。省略了这些开销性能会成倍的提高。如果这些小函数被上千上万次的调用,那么这样优化下来的效果就非常明显了。这也就是Java在有的时候比C++更快的原因之一。当然,Java做优化实际上相当复杂,因为“大胆”优化有时候也会出现问题。比如将B的Foo()的inlining了,结果突然的蹦出一个对A的Foo()的调用,那程序岂不是要出问题?这个时候呢,Java要退一步,进行反优化(De-optimization),以确保程序的正确。

    就这样,Java的虚拟机“骑着毛驴看账本---走着瞧”。一边执行,一边优化,运行不停,优化不止。从外表上看,Java的程序执行会不停的有小的波动。我说的动态优化/反优化就是原因之一(还有很多其他原因)。Java这种特性非常适合长时间运行的服务器端程序,因为这样Hotspot才有足够的机会对程序进行优化。如果程序只是简单的“Hello world”,那Hotspot一点忙帮不上。并且对于“Hello world”这么个简单的程序,一个Java VM也要启动,这就像你点着了一台锅炉,只是想煎一个鸡蛋。好多人觉得Java慢,最初的影像可能就是来源于此。

    有人这时候一定会问:“既然这样,那为什么Hotspot不对程序就行全盘优化,那样岂不是更好?”。问题是这样的,优化是有代价的。比如一段程序运行要2毫秒,优化要10毫秒。如果这段程序的执行密度很低,那么Hotspot就会觉得优化不划算而不予优化。你不妨这样想,Hotspot是一个精明的商人,赔本的生意它绝对不会做。

    最后再指出一点,那就是Hotspot有客户机和服务器两套(-client, -server),它们有不同的优化方针和策略,具体如何,你可以看看Sun的技术文档。

    .NET的JIT

    .NET的开发研制是在Java之后,它应该仔细研究过Java的各种有缺点。说实在的,当听说.NET采用的是JIT技术的时候,我有些吃惊。因为这种技术Java早期曾经采用过,后来被Hotspot取代了。不知道为什么微软会捡起来。

    点看全图

    外链图片需谨慎,可能会被源头改

    JIT的工作流程大概是这样的。当它载入一个Type的时候(近似为Java的类),它对这个Type里所有的函数都生成一个Stub。大家可以大概想象为“空壳函数”。当程序执行调用到某一个函数的时候,.NET的虚拟机(CLR, Common Language Runtime)将任务转交给JIT。JIT将这个函数的实体IL代码现场编译成本地机器语言(还要根据.NET的Meta Data),然后将这段程序插入到“空壳函数”中去。从此往后,所有对该函数的调用就是在执行这段机器代码。很简单是吧!至于那些从来没有被调用的函数,CLR也不去理睬它们,任凭那个“空壳函数”挂在那里“随风荡漾”。

    就我个人看法,我觉得这种技术比Java的Hotspot要落后,无法达到Java那种“动态优化”的效果。JIT一次编译完成后优化就那样了,不会根据程序运行的特点进行不断的跟踪和调整。

    JIT是要花时间的,这会影响程序的性能。并且JIT生成的本地机器代码存储在该程序的内存空间。程序一旦退出,这些代码就消失了。下回你启动运行这个程序,JIT要重复以前的工作。同样的工作反复进行是一种浪费。这些情况微软很清楚。微软的解决办法是"Install-time compilation",或者叫Pre-JIT。在安装.NET Framework的时候,除了将一个个的Assembly拷贝到你的机器上并登记注册外,一个叫做NGen的程序还会将这些Assembly悄悄编译成本地代码,放置在一个秘密的地方(NGen Cache)。这样今后在你的程序调用.NET系统的函数的时候,JIT就不用现场为你编译了,.NET会使用那些事先编译好的本地代码。当然如果你愿意,你可以将你自己开发的应用程序用NGen处理一下,生成Native code。

    点看全图

    外链图片需谨慎,可能会被源头改

    使用NGen有一些注意事项,尤其是.NET 2.0这部分变化比较大,你在使用前自己要好好看看技术文档。

    那.NET的程序能跨平台吗?有人一定会问。

    答案是可以的。虽然可供跨的平台很有限。比如你可以将你在32位Windows平台上开发的.NET程序直接搬到64位Windows平台上去。你的程序立刻就是64位的了。注意,这一点和其他32位程序不一样,那些程序是在64位平台上的一个微环境里运行(叫做Wow64),它们还是32位程序。而你的.NET程序却是正儿八经的64位程序了。这都是托.NET虚拟机的福。不过你先别高兴太早,要做到这点,你写程序的时候还是有一些制约的。比如你如果在.NET程序里调用了32位的COM程序或是其它32位Native Code,那么你的程序就不可能变成64位的程序了。原因吗,那就要说到64位Windows了,不过那就又是一个大话题了,这里且按下不表。

    ================================================================================

    好了,看到这里,大家可能对Java和.NET的程序运行有了一个了解了。大家是不是觉得微软的.NET有些“面”啊?

    我自己认为Technology approach是一个问题,具体的Implementation又是一个问题。评判Java和.NET那个设计的更好不是件简单的事情。就现在的情况来看,这两位各有千秋。比如说Java的Object serialization/de-serialization就做的很出色,.NET built-in的object还不错,但一旦serialization/de-serialization User-defined的Object,问题马上就来了。数据量一大,程序Painful slow。为什么这样我不知道,也是只是.NET 2.0 Beta版本的一个Bug(希望是这样)。而另一方面呢,.NET的Generics非常的先进,是彻头彻尾的Generics,彻底消除了Boxing/Un-boxing和Down-Casting,这方面的性能迅猛提高。而Java的Generics不过是一个“障眼法”,和.NET根本没法比。

    Competition makes everybody better。有竞争才会有发展,这是一个最浅显的道理。希望Java和.NET这对冤家对头能相互砥砺,不断的提高,呈现给我们更好的技术。那才是我们广大程序员的福祉!

    [MOVE]Long life programming!!![/MOVE]

    点看全图

    外链图片需谨慎,可能会被源头改

    元宝推荐:ArKrXe,Highway, 通宝推:老醋花生,

    本帖一共被 1 帖 引用 (帖内工具实现)
    • 家园 这个怎么没推荐?

      好东西不要不好意思多给人看嘛.

      • 家园 当版主的唯一不便之处是不好意思给自己的好帖子加精,

        这实际上会造成一些好帖子不能方便地被会员找到。

        应该有一个机制,找一些人,他们专门给版主且也只能给版主加精。

      • 家园 那你赶紧帮着推荐
    • 家园 【补充】.NET设计师谈Hotspot和.NET的JIT

      Anders Hejlsberg是我崇敬的一位IT大师,他是微软十虎将之一(Ten distinguished engineer),他是丹麦人,一个人搞出了Pascal,后来又领导开发了Delphi,VJ++。现在他领导着微软的C#团队,并且是.NET核心创始人之一。我听过他的不少讲话,非常诚恳,坦率和幽默。兄弟我很是佩服!

      点看全图

      外链图片需谨慎,可能会被源头改

      下面是他在接受采访时对话的节选。建议看看。

      Bill Venners: One difference between Java bytecodes and IL [Intermediate Language] is that Java bytecodes have type information embedded in the instructions, and IL does not. For example, Java has several add instructions: iadd adds two ints, ladd adds two longs, fadd adds two floats, and and dadd adds two doubles. IL has add to add two numbers, add.ovf to add two numbers and trap signed overflow, and add.ovf.un to add two numbers and trap unsigned overflow. All of these instructions pop two values off the top of the stack, add them, and push the result back. But in the case of Java, the instruction indicates the type of the operands. A fadd means two floats are sitting on the top of the stack. A ladd means there two longs are sitting on the top of the stack. By contrast, the CLR's [Common Language Runtime] add instructions are polymorphic, they add the two values on the top of the stack, whatever their type, although the trap overflow versions differentiate between signed and unsigned. Basically, the engine running IL code must keep track of the types of the values on the stack, so when it encounters an add, it knows which kind of addition to perform.

      I read that Microsoft decided that IL will always be compiled, never interpreted. How does encoding type information in instructions help interpreters run more efficiently?

      Anders Hejlsberg: If an interpreter can just blindly do what the instructions say without needing to track what's at the top of the stack, it can go faster. When it sees an iadd, for example, the interpreter doesn't first have to figure out which kind of add it is, it knows it's an integer add. Assuming someone has already verified that the stack looks correct, it's safe to cut some time there, and you care about that for an interpreter. In our case, though, we never intended to target an interpreted scenario with the CLR. We intended to always JIT [Just-in-time compile], and for the purposes of the JIT, we needed to track the type information anyway. Since we already have the type information, it doesn't actually buy us anything to put it in the instructions.

      Bill Venners: Many modern JVMs [Java virtual machines] do adaptive optimization, where they start by interpreting bytecodes. They profile the app as it runs to find the 10% to 20% of the code that is executed 80% to 90% of the time, then they compile that to native. They don't necessarily just-in-time compile those bytecodes, though. A method's bytecodes can still be executed by the interpreter as they are being compiled to native and optimized in the background. When native code is ready, it can replace the bytecodes. By not targeting an interpreted scenario, have you completely ruled out that approach to execution in a CLR?

      Anders Hejlsberg: No, we haven't completely ruled that out. We can still interpret. We're just not optimized for interpreting. We're not optimized for writing that highest performance interpreter that will only ever interpret. I don't think anyone does that anymore. For a set top box 10 years ago, that might have been interesting. But it's no longer interesting. JIT technologies have gotten to the point where you can have multiple possible JIT strategies. You can even imagine using a fast JIT that just rips quickly, and then when we discover that we're executing a particular method all the time, using another JIT that spends a little more time and does a better job of optimizing. There's so much more you can do JIT-wise.

      Bill Venners: When I asked you earlier (In Part IV) about why non-virtual methods are the default in C#, one of your reasons was performance. You said:

      We can observe that as people write code in Java, they forget to mark their methods final. Therefore, those methods are virtual. Because they're virtual, they don't perform as well. There's just performance overhead associated with being a virtual method.

      Another thing that happens in the adaptive optimizing JVMs is they'll inline virtual method invocations, because a lot of times only one or two implementations are actually being used.

      Anders Hejlsberg: They can never inline a virtual method invocation.

      Bill Venners: My understanding is that these JVM's first check if the type of the object on which a virtual method call is about to be made is the same as the one or two they expect, and if so, they can just plow on ahead through the inlined code.

      Anders Hejlsberg: Oh, yes. You can optimize for the case you saw last time and check whether it is the same as the last one, and then you just jump straight there. But there's always some overhead, though you can bring the overhead down to fairly minimum.

    • 家园 受教,花一吨
    • 家园 献花。深入浅出,版主出手不凡!

      对串行化和范型的看法和我完全一致。

    • 家园 花一吨。资源需求方面的要求怎样?
      • 家园 都不是“省油灯”!

        可能现在资源太便宜了吧,人们对Resource似乎变得Careless。网上关于Java和.NET的报道对这一方面都不怎么重视。

        Java和.NET都有Garbage collector。虽然算法不太一样(非常有讲究,是影响程序性能的一个关键),但任务是相同的。在资源感到有压力的时候,开始清理Heap。那什么时候“资源感到有压力”呢?这个就跟具体的机器配置有关了。清理Heap,回收内存,重新安排Ojbect layout要消耗CPU时间的。所以如果可能,Java和.NET的Garbage collector尽量不出动。这给资源消耗的评测带来了一些困难。

        我自己的感觉是它们俩是一个量级的。

        • 家园 现在计算机的处理能力的增长速度已经大大地减慢了。

          如果不考虑资源的优化问题,迟早会出问题的。

        • 家园 送花。感觉你偏爱java。一个Garbage collector问题。

          在其他论坛上看到C++老手们大贬Garbage collector,死也不愿意转到java或.net上来。

          我只觉得有了垃圾收集器,编程变得简单了,何乐不为呢。

          想听听你的看法。

          • 家园 其实C++中使用Smart pointer

            可以大部分缓解没有GC的问题,减少程序操作指针的错误。

            当然GC是一个非常好的feature,如果resource不是critical的话。

          • 家园 Garbage collector的效率

            与程序类型/风格有关.最早使用Garbage collector

            的是Functional languages. 这类语言的程序有大量

            short-lived small objects, Garbage collector很

            有兢争力.Mark-and-sweep虽然可能比malloc/free

            慢一点,但是可以有效解决fragmentation.Copying

            collection(尤其是generational GC)则进一步在速度上

            也有一争.

            另外,比较效率也要从整体上比.举一个例子.我正在

            写的一个程序有一个producer和多个independent consumer.

            每个object都要接收多个consumer的处理,也可能被一个

            consumer传给另一个consumer,还可能需要存下来.如果没有GC,要么用

            single-thread固定流程,要么create one copy for each consumer,

            要么每个consumer都要执行reference-counting routine以

            决定是否free一个object.每个选择都对系统的结构/效率有不

            好的影响.

            我觉得GC被贬的原因主要有:

            1. GC 不适合memory-scarce environment.

            2. GC memory footprint 较大影响cache效率. (不过对绝大多数

            人, 包括C programmers, 比cache效率大的问题多了去了.)

            3. 早期GC implementation对Stack处理不好造成retention.

            元宝推荐:Highway,

            本帖一共被 1 帖 引用 (帖内工具实现)
          • 家园 我来举个实例,支持垃圾收集器

            某项目中,我同事的任务是在Unix下用ANSI C做开发,需要用到malloc()和free()做内存的动态分配和释放。编码完成之后,他在某处碰到coredump问题,整整花了三天时间,没有找出原因,整个人状态极差,萎靡不振。我发觉了,预感这样下去这家伙会辞职。于是,主动要求帮他查错,花了两三个小时之后,我终于定位到问题所在:不该用free()的地方错调了。改了那个错,他的程序一泻千里,顺利通过。可是,不出我所料,他提出辞职,原因是:不能做好该类开发工作。

            作为一个首次用C语言在Unix下开发的程序员,他的表现其实已经很令我满意,至今我都认为他是个优秀的程序员。可是,因为这该死的free,他大为自责,最终还是辞职了。

            所以,无论编程人员水平如何,简化编程都很有意义。

            诋毁Garbage collector的人,只是为了保持自己的优越感……

分页树展主题 · 全看 下页


有趣有益,互惠互利;开阔视野,博采众长。
虚拟的网络,真实的人。天南地北客,相逢皆朋友

Copyright © cchere 西西河