何时Java比C++更快(或何时JIT比预编译更快)?

22

可能重复:
JIT编译器与离线编译器

我听说在某些情况下,Java程序或者说Java程序的某些部分由于JIT优化可以比C++(或其他预编译代码)执行得更快。这是因为编译器能够确定一些变量的作用域,在运行时避免一些条件和进行类似的技巧。

您能否给出一个(或更好的 - 一些)例子来说明这种情况?并且可能概述编译器能够优化字节码超过预编译代码的确切条件吗?

注意:这个问题不是关于Java和C++的比较。它是关于JIT编译的可能性。请勿争吵。如果有任何重复,请指出。


这个内容实际上是重复的。对此造成的不便请谅解。请合并。 - kostja
4个回答

42

实际上,在以下情况下(我个人观察到的所有情况),你可能会发现天真编写的Java代码在性能上优于天真编写的C++代码:

  • 大量小内存分配/释放。主要的JVM具有非常高效的内存子系统,垃圾回收可能比需要显式释放更有效(而且如果确实需要,它可以移动内存地址之类的)。

  • 通过深层次的方法调用进行高效访问。JVM非常擅长省略任何不必要的东西,在我的经验中通常比大多数C++编译器(包括gcc和icc)好。部分原因是因为它可以在运行时进行动态分析(即可以过度优化,并且只有在检测到问题时才进行反优化)。

  • 将功能封装到短暂小对象中。

在每种情况下,如果你付出努力,C++可能会更好(在自由列表和块分配/释放内存方面,C++可以在几乎每个具体案例中击败JVM的内存系统;通过额外的代码、模板和聪明的宏,可以非常有效地折叠调用堆栈;在C++中,你可以拥有小的部分初始化的堆栈分配对象,这些对象比JVM的短暂对象模型更有效)。但你可能不想付出这么大的努力。


1
谢谢。这正是我所希望的详细程度。 - kostja

7

一些例子:

  • JIT编译器可以使用最新的SSE扩展生成非常特定于CPU的机器码,而这些码不会用于需要在广泛的CPU上运行的预编译代码。
  • JIT知道什么时候一个虚拟方法(Java中的默认方式)没有在任何地方被重写,因此可以内联(尽管这需要能够在加载新类覆盖该方法时取消内联;当前Java JIT编译器实际执行此操作)。
  • 与此相关的是,逃逸分析允许进行几种特定于情况的优化。

1
第一点是另外一个有效的观点,因为许多Java库是在新的CPU架构出现之前编写的。这些旧的库仍然可以利用最新的CPU改进。要想在C++中利用最新的体系结构,必须能够从源代码进行编译,这对于第三方库来说可能是不可能或者不实际的,特别是开发人员不是最终用户的情况下。例如,如果您有一个必须部署到许多不同类型的PC上的应用程序,那么发布每个可能的平台可能会很困难,因此通常选择最低公共分母。 - Peter Lawrey
我相信JIT可以执行多态内联,即它知道最多两个可能被调用的“虚拟”方法,并且这些方法可以被内联,如果对象不是这些类之一,则会有一个回退。这意味着即使具有多个可能实现的虚拟方法也可以根据运行时行为进行内联。 - Peter Lawrey
1
C++的基于剖面的优化器也使用这些技巧。 - Ben Voigt

6

维基百科:http://en.wikipedia.org/wiki/Just-in-time_compilation#Overview

此外,它在某些情况下可以比静态编译提供更好的性能,因为许多优化只有在运行时才可行:

  1. 编译可以被优化到目标CPU和操作系统模型,例如JIT可以在检测到CPU支持SSE2指令时选择使用。要获得这种优化特定性,静态编译器必须为每个预期平台/架构编译二进制文件,或者在单个二进制文件中包含代码部分的多个版本。

  2. 系统能够收集程序在其所处环境中实际运行的统计信息,并且可以重新排列和重新编译以获得最佳性能。但是,一些静态编译器也可以将配置文件信息作为输入。

  3. 系统可以进行全局代码优化(例如库函数的内联),而不会失去动态链接的优势并且不会产生静态编译器和链接器固有的开销。具体来说,在进行全局内联替换时,静态编译过程可能需要运行时检查,并确保如果对象的实际类覆盖了内联方法,则会发生虚拟调用,并且在循环内可能需要处理数组访问的边界条件检查。使用即时编译,在许多情况下,此处理可以移出循环,通常可以大大提高速度。

  4. 虽然静态编译的垃圾收集语言也可以做到这一点,但字节码系统可以更轻松地重新排列执行代码以获得更好的缓存利用率。


4
信息很好,但仔细阅读后发现预编译实际上可以并且确实会执行许多仅限于JIT的优化。 - Ben Voigt
3
@BenVoigt 好观点。主要的剩余争论在于JIT可以访问特定于运行进程的信息,而不是预先创建的配置文件或类似物。因此,它可以更频繁地执行大胆的优化,并具有更高的成功机会。 - Aleksandr Dubinsky

0

一个好的链接有时候就像一个完美答案一样,非常有洞见。谢谢。 - kostja
5
好的,您说的是一个很好的“草人”论点。C++并不要求您使用malloc,例如Wireshark的池分配器就是一个很好的例子。 - Ben Voigt
那么这个论点不适用于C++使用new进行分配内存吗?这是因为HeapAlloc等函数分配的堆被用于new的分配吗? - kostja

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接