16

我最近看到了多个关于 Java(和基于 JVM 的语言,如 Scala)如何在性能上与 C/C++ 代码相媲美的说法。

例如,来自ScalaLab 项目的描述:

基于 Scala 的脚本编写速度,接近原生和优化的 Java 代码的速度,因此接近甚至优于基于 C/C++ 的科学代码!

有人可以指出这些 JVM 优化是什么的摘要吗?是否有任何真正的基准支持这种说法或提供一些现实世界的比较?

4

3 回答 3

23

表演技巧

首先,这取决于您谈论的 JVM,因为有多个 - 但我假设您的意思是 Oracle HotSpot(无论如何,其他顶级 JVM 将使用类似的技术)。

对于那个 JVM, HotSpot 内部 wiki 中的这个列表提供了一个很好的开始(子页面详细介绍了一些更有趣的技术)。如果您只是在寻找技巧的洗衣清单,那么 wiki也有,尽管要理解它们,您可能必须在谷歌上搜索各个术语。

并非所有这些都是最近实现的,但一些大的已经实现(范围检查省略、转义分析、超词优化)——至少对于“最近”的松散定义。

接下来,让我们看一下 C/C++ 与 Java 的相对性能图,以及为什么上述技术有助于缩小差距,或者在某些情况下实际上使 Java 和固有的优势优于原生编译语言。

Java 与 C/C++

在高层次上,优化是您在任何像 C 和 C++ 等本地语言的体面编译器中看到的东西的混合,以及减少 Java/JVM 特定功能和安全检查的影响所需的东西,例如作为:

  • 逃逸分析,可以(在某种程度上)减轻对象的无堆栈分配
  • 内联缓存和类层次结构分析,可缓解“每个功能都是虚拟的”
  • 范围检查消除,这减轻了“每个数组访问都经过范围检查”

许多这些 JVM 特定* 优化仅有助于使 JVM 与本机语言相提并论,因为它们正在解决本机语言不必处理的障碍。然而,一些优化是静态编译语言无法管理的(或者在某些情况下只能通过配置文件引导的优化来管理,这种情况很少见,而且无论如何都必须是一刀切的):

  • 仅动态内联最热门的代码
  • 基于实际分支/开关频率的代码生成
  • 动态生成 CPU/指令集感知代码(甚至在代码编译后发布的 CPU 功能!)1
  • 删除从未执行的代码
  • 注入与应用程序代码交错的预取指令
  • 安全指向支持的整个技术系列

共识似乎是,Java 生成的代码通常在速度上与中等优化级别的优秀 C++ 编译器相似,例如 gcc -O2,尽管很大程度上取决于确切的基准。像 HotSpot 这样的现代 JVM 往往在低级数组遍历和数学方面表现出色(只要竞争的编译器没有向量化 - 这很难被击败),或者在竞争代码执行相似数量分配时具有大量对象分配的场景中(JVM 对象分配 + GC 通常比 malloc 快),但是当典型的 Java 应用程序的内存损失是一个因素时,堆栈分配被大量使用,或者向量化编译器或内在函数向本机代码倾斜时,就会下降。

如果您搜索 Java 与 C 的性能,您会发现很多人都解决了这个问题,他们的严谨程度各不相同。这是我偶然发现的第一个,它似乎显示了 gcc 和 HotSpot 之间的粗略联系(即使在这种情况下为 -O3 )。如果您想了解单个基准如何在每种语言中经历多次迭代、相互跨越,这篇文章和相关讨论可能是一个更好的开始 - 并展示了双方优化的一些限制。

*并不是真正的 JVM 特定的——大多数也适用于其他安全或托管语言,如 CLR


1随着新指令集(尤其是 SIMD 指令,但还有其他指令)以某种频率发布,这种特殊的优化变得越来越重要。自动向量化可以大大加快一些代码的速度,虽然 Java 在这方面的速度已经很慢,但它们至少在赶上一点。

于 2013-04-22T03:40:32.980 回答
13

当然,实际性能取决于基准,并因应用程序而异。但是很容易看出 JIT VM 是如何与静态编译的代码一样快的,至少在理论上是这样。

JIT 代码的主要优势在于它可以基于仅在运行时才知道的信息进行优化。在 C 中,当您链接到 DLL 时,您必须每次都调用该函数。在动态语言中,即使是在运行时加载的函数,也可以内联函数,这要归功于即时编译。

另一个例子是基于运行时值进行优化。在 C/C++ 中,您使用预处理器宏来禁用断言,如果您想更改此选项,则必须重新编译。在 Java 中,断言是通过设置一个私有布尔字段然后在代码中放置一个 if 分支来处理的。但是由于 VM 可以根据标志的值编译包含或不包含断言代码的代码版本,因此对性能的影响很小或没有。

另一个主要的 VM 创新是多态内联。Idomatic Java 非常关注像 getter 和 setter 这样的小型包装方法。为了获得良好的性能,内联它们显然是必要的。VM 不仅可以在实际仅调用一种类型的常见情况下内联多态函数,还可以内联调用多种不同类型的代码,方法是在适当的代码中包含内联缓存。如果代码开始在许多不同的类型上运行,VM 可以检测到这一点并回退到较慢的虚拟调度。

静态编译器当然不能做这一切。强大的静态分析只能让您走这么远。这也不仅限于 Java,尽管它是最明显的例子。用于 Javascript 的 Google 的 V8 vm 也非常快。Pypy 旨在为 Python 和 Rubinius 为 Ruby 做同样的事情,但它们并不完全在那里(当你有一个大公司支持你时,它会有所帮助)。

于 2013-04-22T03:38:07.803 回答
1

我要补充一点,热点、jrockit 和 IBM 的 JVM 都在 GC 中执行堆压缩。由于这个原因,我最近将一些繁重的数学代码移植到了 Scala。如果您打算运行任何大型应用程序,我强烈推荐 Java。在部署到服务器或向上扩展时,您可能会后悔使用 CLR,尤其是在内存密集型的情况下。

此外,对于本机代码,JVM 配置选项也非常出色。

于 2013-05-09T07:54:42.793 回答