JVM 上的性能
JVM 上的性能模型有时在有关它的评论中很复杂,因此人们对它了解不多。由于各种原因,某些代码可能不如预期的那样高效或可扩展。在此,我们提供一些示例。
原因之一是 JVM 应用程序的编译过程与静态编译语言的编译过程不同(请参阅 [2])。Java 和 Scala 编译器将源代码转换为 JVM 字节码,并且几乎不进行优化。在大多数现代 JVM 中,一旦程序字节码运行,它就会转换为正在运行它的计算机架构的机器码。这称为即时编译。但是,由于即时编译必须快速,因此代码优化的级别较低。为了避免重新编译,所谓的 HotSpot 编译器仅优化频繁执行的代码部分。这对基准编写者来说意味着,程序每次运行时可能具有不同的性能。在同一 JVM 实例中多次执行同一代码段(例如方法)可能会产生非常不同的性能结果,具体取决于在运行之间是否优化了特定代码。此外,测量某段代码的执行时间可能包括 JIT 编译器本身执行优化的时间,从而产生不一致的结果。
在 JVM 上进行的另一个隐藏执行是自动内存管理。每隔一段时间,程序的执行就会停止并运行垃圾收集器。如果被基准测试的程序分配了任何堆内存(大多数 JVM 程序都会分配),则必须运行垃圾收集器,从而可能扭曲测量结果。为了摊销垃圾回收效果,被测量的程序应该运行多次以触发多次垃圾回收。
性能下降的一个常见原因也是在将基元类型作为参数传递给泛型方法时隐式发生的装箱和拆箱。在运行时,基元类型将转换为表示它们的对象,以便可以将它们传递给具有泛型类型参数的方法。这会引起额外的分配并且速度较慢,还会在堆上产生额外的垃圾。
在涉及并行性能时,一个常见的问题是内存争用,因为程序员无法明确控制对象在何处分配。事实上,由于 GC 的影响,在对象在内存中被移动之后,争用可能会在应用程序生命周期的后期阶段发生。在编写基准时需要考虑此类影响。
微基准测试示例
有几种方法可以在测量过程中避免上述影响。首先,必须执行目标微基准测试足够多次,以确保即时编译器已将其编译为机器码并且已对其进行优化。这称为热身阶段。
微基准测试本身应在单独的 JVM 实例中运行,以减少来自程序不同部分分配的对象的垃圾回收或无关的即时编译产生的噪音。
它应使用 HotSpot JVM 的服务器版本运行,该版本执行更激进的优化。
最后,为了减少基准测试过程中发生垃圾回收的可能性,理想情况下应在基准测试运行之前进行一次垃圾回收周期,尽可能地推迟下一次周期。
有关正确的基准测试示例,您可以在 Scala 库基准测试 中查看源代码。
一个集合多大才能并行化?
这是一个常见问题。答案有些复杂。
并行化有用的集合大小实际上取决于许多因素。其中一些因素(但并非全部)包括
- 机器架构。不同的 CPU 类型具有不同的性能和可扩展性特征。与此正交的是,机器是多核的还是具有通过主板通信的多个处理器。
- JVM 供应商和版本。不同的 VM 在运行时对代码应用不同的优化。它们实现不同的内存管理和同步技术。有些不支持
ForkJoinPool
,而恢复到ThreadPoolExecutor
,从而导致更多开销。 - 每个元素的工作负载。并行操作的函数或谓词决定了每个元素的工作负载有多大。工作负载越小,并行运行时获得加速所需的元素数量就越多。
- 特定集合。例如,
ParArray
和ParTrieMap
具有以不同速度遍历集合的分隔符,这意味着仅在遍历本身中就有更多每个元素的工作。 - 特定操作。例如,
ParVector
对于转换器方法(如filter
)比对于访问器方法(如foreach
)慢得多 - 副作用。在并发修改内存区域或在
foreach
、map
等的主体中使用同步时,可能会发生争用。 - 内存管理。在分配大量对象时,可能会触发垃圾回收周期。根据新对象的引用传递方式,GC 周期可能需要更多或更少的时间。