最近有多个声称,Java(以及基于JVM的语言如Scala)的性能与C/C++代码相媲美。
例如,在ScalaLab项目的描述中:
基于Scala脚本的速度接近于原生和优化过的Java代码,并且因此接近或甚至优于基于C/C++的科学代码!
有人可以给我指出JVM是如何进行这些优化的吗?是否有任何真实的基准支持这个声明或提供一些真实世界的比较呢?
最近有多个声称,Java(以及基于JVM的语言如Scala)的性能与C/C++代码相媲美。
例如,在ScalaLab项目的描述中:
基于Scala脚本的速度接近于原生和优化过的Java代码,并且因此接近或甚至优于基于C/C++的科学代码!
有人可以给我指出JVM是如何进行这些优化的吗?是否有任何真实的基准支持这个声明或提供一些真实世界的比较呢?
首先,这取决于你所指的 JVM 是哪个,因为有几个 - 但我假设你指的是 Oracle HotSpot(而且无论如何,其他顶级 JVM 将使用类似的技术)。
对于该 JVM,HotSpot 内部 wiki 的此列表提供了一个很好的起点(子页面详细介绍了一些更有趣的技术)。如果你只是在寻找一份技巧清单,wiki 也提供了这个,尽管要理解它们,你可能需要搜索各个专业术语。
并非所有这些技巧都是最近实现的,但其中一些重要的确实已经实现了(例如范围检查省略、逃逸分析、超字优化)- 至少对于“最近”的松散定义来说。
接下来让我们看一下 C/C++ 与 Java 相对性能图像,以及为什么上面的技术可以帮助缩小差距,或在某些情况下实际上给予 Java 比本地编译语言更本质的优势。
在较高层面上,优化是一种混合体,其中包括您在任何像C和C++这样的本地语言中看到的优化,以及需要减少Java/JVM特定功能和安全检查的影响的优化,例如:
其中许多JVM特定*优化只有帮助将JVM提升到与本地语言相同的水平,因为它们正在解决本地语言不必处理的障碍。但是,一些优化是静态编译语言无法处理的事情(或者在某些情况下仅能通过基于配置文件的优化管理,这是罕见的,并且无论如何都是一种一刀切的方法):
普遍认为,Java通常在适度优化级别(如gcc -O2)下产生类似于良好C++编译器速度的代码,尽管很多取决于确切的基准测试。像HotSpot这样的现代JVM在低级数组遍历和数学方面表现出色(只要竞争编译器不是矢量化-那很难被击败),或者在具有大量对象分配的情况下,当竞争代码做了类似数量的分配时(JVM对象分配+GC通常比malloc更快),但在典型Java应用程序的内存惩罚是一个因素的情况下,堆栈分配被广泛使用,或者矢量化编译器或内部函数将天平倾向于本地代码时会失败。
如果你搜索Java vs C的性能比较,你会发现有很多人探讨这个问题,但严谨程度不尽相同。这是我偶然发现的第一个,它似乎显示了gcc和HotSpot之间的粗略平局(即使在这种情况下使用-O3)。如果你想看到单个基准测试如何在每种语言中经历数次迭代并相互超越,并展示双方优化的一些限制,那么这篇文章和相关讨论可能是更好的起点。1 这种优化越来越重要,因为新的指令集(特别是SIMD指令,但也有其他)正在频繁发布。自动向量化可以大大加速某些代码,尽管Java在这方面起步较慢,但他们至少正在迎头赶上。
当然,实际性能取决于基准测试,并且根据应用程序而异。但是很容易看出JIT VM可以在理论上与静态编译的代码一样快。
JIT代码的主要优势是它可以基于仅在运行时已知的信息进行优化。 在C语言中,当您链接到DLL时,每次都必须进行函数调用。 在动态语言中,即使是在运行时加载的函数也可以被内联,这得益于即时编译。
另一个例子是基于运行时值进行优化。 在C / C ++中,您使用预处理器宏来禁用断言并且必须重新编译以更改此选项。 在Java中,通过设置一个私有布尔字段,然后将if分支放入代码中来处理断言。 但由于VM可以编译版本的代码,该代码根据标志值包括或不包括assert代码,因此几乎没有任何性能损失。
另一个主要的VM创新是多态内联。 典型的Java非常依赖小的包装器方法,比如getter和setter。为了获得良好的性能,内联它们显然是必要的。 虚拟机不仅可以在通常只调用一个类型的情况下内联多态函数,还可以通过包含适当代码的内联缓存来内联调用多个不同类型的代码。 如果代码开始在许多不同的类型上运行,VM可以检测到并回退到更慢的虚拟调度。
静态编译器当然无法做到这一点。 强大的静态分析只能帮助您到达一定程度。 这不仅限于Java语言,尽管它是最明显的例子。 Google的JavaScript V8 VM也非常快。 Pypy旨在为Python实现相同的功能,Rubinius旨在为Ruby实现相同的功能,但它们还没有完全实现(当你有一个大公司支持你时会更有帮助)。