Java中Valhalla项目提升近10倍性能

Valhalla 项目是针对 JVM 性能的重要项目,旨在优化内存布局和提高性能。该项目旨在支持自定义“基元”或值对象的创建,以获得更好的内存局部性和更高的性能。

在测试中,Valhalla 项目展现出了排序速度快9.7倍和累加器速度快12.5倍的惊人性能提升。

背景:Java 的内存模型及其对性能的影响
让我们看一下原始int[]和Integer[]的内存布局之间的区别:

  • Java 中的每个对象都会在对象的开头保留一些元数据(也称为标头)。
  • 对于原始情况,即int[],我们只有一个对象(数组本身),并且单元格连续存储在内存中。
  • 对于非原始情况,即Integer[],我们有 5 个对象(数组和每个整数一个对象),并且“指针”连续存储,而实际数据很可能存储在内存中的某个位置以不连续的方式。这对性能不利。

非原始情况的对象存储对性能不利,因为:

  • 访问整数(非原始)的值包括一次额外的内存访问
  • 当我们从内存中读取一个值时,我们会读取 X 个字节,即使我们要读取的值(本例中为 int,4 个字节)小于 X。
  • X 在大多数现代 CPU 中为 64,也称为 "缓存线",是从内存中获取数据时的操作单位。

举例来说,当我们访问原始数组中的 a[1] 时,我们也会从内存中调用它前面的一些单元格和后面的一些单元格。

对于非原始(整数)数组,我们也会获取其前后的单元格,但这些单元格包含 "指针",然后当我们访问实际数据时,例如 "1 "的单元格,我们会获取其前后的一些地址,但这些地址对我们来说很可能是无用的,它们不会包含任何其他单元格。

  • 每个对象的元数据(标头)在 96-128bit 之间(在小人国项目之前),因此对于整数数组的每个单元来说,只需要 4 个字节,但我们却额外浪费了 12-16 个字节!这还会影响延迟,因为这些元数据也会被提取到 CPU 缓存中,因此我们能容纳的实际数据更少,需要访问主内存的次数也更多。

在 1995 年 Java 第一个版本问世时,上述情况还算可以,因为那时内存和 CPU 的速度差不多。

但在现代,CPU 的速度远远快于内存的速度,因此这种内存布局严重影响了性能。

什么Valhalla项目?
早在 2014 年,JEP-401 项目就已开始构思,这应该能让您了解到该项目有多么复杂,它旨在支持创建自定义 "基元 "或值对象。

这基本上意味着,让用户能够扁平化自己的对象,并享受基元的性能,但同时也会受到一些限制,我就不详细介绍了,因为该项目还未进入预览阶段,所以这些限制可能会发生变化。

让我们看看给定类 Point 的数组的内存布局将如何变化:

// Before
public class Point {
  private final int x;
  private final int y;
}

// After
public primitive class Point {
  private final int x;
  private final int y;
}

这极大地提高了内存的局部性,现在当我们想遍历所有点时,访问 p[0].x 时获取的缓存行将包含我们即将访问的一些单元格,如 p[0].y、p[1].x 等。

另一个最大的好处是节省内存,现在我们只需保留 1 个对象头(元数据),而不是 4 个。

Valhalla项目性能
我很想在集合中测试这些原始类,但由于 Valhalla 项目目前还只是勉强进入早期使用阶段,所以还不支持这些原始类。

我们将测试两种情况:

  • 对 100 万个点进行排序,主要按 X 排序,次要按 Y 排序
  • 使用不可变累加器计算 100 万个点的总和

我们将在我的笔记本电脑上运行:
  • Java 22-valhalla
  • 64 GB 内存
  • 英特尔(R)酷睿(TM)i7-11850H 处理器

排序速度是原来的 9.7 倍,而累加器速度是原来的 12.5 倍!.....!

最让我印象深刻的其实不是这些数字本身,而是像 Java 这样古老的语言仍然可以做出如此根本性的改变,产生如此重大的影响

目前正在进行的所有令人惊叹的项目将在未来把 Java 带到何方,让我们拭目以待。