JVM 是否跳过临时通用转换
does JVM skip a temporary generic conversion
我正在寻找一种通过通用接口使用原始集合的方法。
在 IntArray class 和 scenario 函数的情况下,JVM 会创建临时的 Integer 对象,或者直接传递一个 int?
元素存储在原语 int[] 中,并且仅直接分配给原语 int,因此未经优化,意味着不必要的对象创建, 只是为了在几分之一秒内摧毁它。
public class Test {
private interface Array<E> {
E get(int index);
void set(int index, E element);
}
private static class GenericArray<E> implements Array<E> {
private final E[] elements;
@SuppressWarnings("unchecked")
public GenericArray(int capacity) {
this.elements = (E[]) new Object[capacity];
}
@Override
public E get(int index) {
return elements[index];
}
@Override
public void set(int index, E element) {
elements[index] = element;
}
}
private static class IntArray<E> implements Array<Integer> {
private final int[] elements; // primitive int array
public IntArray(int capacity) {
this.elements = new int[capacity];
}
@Override
public Integer get(int index) {
return elements[index];
}
@Override
public void set(int index, Integer element) {
elements[index] = element;
}
}
private static void scenario(Array<Integer> array) {
int element = 256;
array.set(16, element); // primitive int given
element = array.get(16); // converted directly to primitive int
System.out.println(element);
}
public static void main(String[] args) {
Array<Integer> genericArray = new GenericArray<>(64);
Array<Integer> primitiveArray = new IntArray<>(64);
scenario(genericArray);
scenario(primitiveArray);
}
}
„…does JVM skip a temporary generic conversion…“
没有。它没有。它做了 Boxing Conversion…
…
5.1.7 Boxing Conversion
Boxing conversion treats expressions of a primitive type as expressions of a corresponding reference type. Specifically, the following nine conversions are called the boxing conversions:
…
- From type int to type Integer
…
…还有一个Unboxing Conversion…
…
5.1.8. Unboxing Conversion
Unboxing conversion treats expressions of a reference type as expressions of a corresponding primitive type. Specifically, the following eight conversions are called the unboxing conversions:
…
- From type Integer to type int
…
参见Java Tutorial's Autoboxing and Unboxing trail。
„…will JVM create temporary Integer objects, or directly pass an int?…“
两者兼而有之。首先是前者,最后是后者。
„…unnecessary object creation, just to destroy it in a fraction of second…“
设计 Java 语言的 Oracle 架构师可能同意您的观点……
„…The problem with boxing is that it is [ad-hoc] and expensive; extensive work has gone on under the hood to address both of these concerns…“ — Brian Goetz, State of Valhalla, March 2020
他们已经为您的请求提供了解决方案……
„…for a way to use primitive collections with generic interfaces…“
…如果你有耐心…
„…We are trying to preserve room for specialized generics as a future feature… We wish to reserve a natural notation in the future for specialized types, such as List<int>
…“ — Migration: specialized generics, Brian Goetz, State of Valhalla
专门的仿制药还没有出现。但是你可以 take other early-access features of Valhalla out for a spin today.
Java 没有基本类型的泛型 (yet)。
您的 IntArray
处理 Integer
对象,至少在字节码级别。如果我们反编译 class,我们会清楚地看到对装箱 Integer.valueOf
和拆箱 Integer.intValue
方法的调用:
javap -c -private Test$IntArray
public java.lang.Integer get(int);
Code:
0: aload_0
1: getfield #2 // Field elements:[I
4: iload_1
5: iaload
6: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: areturn
public void set(int, java.lang.Integer);
Code:
0: aload_0
1: getfield #2 // Field elements:[I
4: iload_1
5: aload_2
6: invokevirtual #4 // Method java/lang/Integer.intValue:()I
9: iastore
10: return
然而,JIT 编译器有一个优化来消除冗余 boxing-unboxing 对:-XX:+EliminateAutoBox
。默认情况下优化是开启的,但不幸的是并不总是有效。在 JMH 基准测试的帮助下,让我们看看它是否适用于您的情况。
package bench;
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class GenericArrays {
Array<Integer> genericArray = new GenericArray<>(64);
Array<Integer> primitiveArray = new IntArray(64);
int n;
@Setup
public void setup() {
for (int i = 0; i < 64; i++) {
genericArray.set(i, i + 256);
primitiveArray.set(i, i + 256);
}
}
@Benchmark
public int getGeneric() {
return genericArray.get(n++ & 63);
}
@Benchmark
public int getPrimitive() {
return primitiveArray.get(n++ & 63);
}
@Benchmark
@Fork(jvmArgsAppend = "-XX:-EliminateAutoBox")
public int getPrimitiveNoOpt() {
return primitiveArray.get(n++ & 63);
}
@Benchmark
public void setGeneric() {
genericArray.set(n++ & 63, n);
}
@Benchmark
public void setPrimitive() {
primitiveArray.set(n++ & 63, n);
}
@Benchmark
@Fork(jvmArgsAppend = "-XX:-EliminateAutoBox")
public void setPrimitiveNoOpt() {
primitiveArray.set(n++ & 63, n);
}
private interface Array<E> {
E get(int index);
void set(int index, E element);
}
static class GenericArray<E> implements Array<E> {
private final E[] elements;
@SuppressWarnings("unchecked")
public GenericArray(int capacity) {
this.elements = (E[]) new Object[capacity];
}
@Override
public E get(int index) {
return elements[index];
}
@Override
public void set(int index, E element) {
elements[index] = element;
}
}
static class IntArray implements Array<Integer> {
private final int[] elements;
public IntArray(int capacity) {
this.elements = new int[capacity];
}
@Override
public Integer get(int index) {
return elements[index];
}
@Override
public void set(int index, Integer element) {
elements[index] = element;
}
}
}
当 运行 在 JDK 14.0.2 上进行基准测试时,我得到以下分数(越低越好)。
Benchmark Mode Cnt Score Error Units
GenericArrays.getGeneric avgt 20 3,769 ± 0,039 ns/op
GenericArrays.getPrimitive avgt 20 3,445 ± 0,037 ns/op
GenericArrays.getPrimitiveNoOpt avgt 20 5,147 ± 0,073 ns/op
GenericArrays.setGeneric avgt 20 10,491 ± 0,055 ns/op
GenericArrays.setPrimitive avgt 20 3,896 ± 0,023 ns/op
GenericArrays.setPrimitiveNoOpt avgt 20 4,078 ± 0,077 ns/op
这使我们得出两个观察结果:
- 原始数组似乎表现更好;
EliminateAutoBox
优化显然有效,因为当优化关闭时,时间会更高。
现在让我们验证优化是否有助于避免不必要的分配。
JMH (-prof gc
) 中内置的 GC 分析器将完成这项工作。
Benchmark Mode Cnt Score Error Units
GenericArrays.getGeneric:·gc.alloc.rate.norm avgt 20 ≈ 10⁻⁵ B/op
GenericArrays.getPrimitive:·gc.alloc.rate.norm avgt 20 ≈ 10⁻⁵ B/op
GenericArrays.getPrimitiveNoOpt:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
GenericArrays.setGeneric:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
GenericArrays.setPrimitive:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
GenericArrays.setPrimitiveNoOpt:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
这里我们看到getPrimitive
基准的分配率为零。这意味着,JVM 能够消除临时 Integer
对象的分配。当优化关闭时,分配率预计为每次操作 16 字节 - 恰好是一个 Integer
对象的大小。
出于某种原因,JVM 无法消除 setPrimitive
中的装箱。正如我之前所说,优化是脆弱的,并非在所有情况下都有效。
但是,setPrimitive
仍然比 setGeneric
快很多。好处在于存储原语比存储引用更有效,因为存储引用通常需要 GC 屏障。
我正在寻找一种通过通用接口使用原始集合的方法。
在 IntArray class 和 scenario 函数的情况下,JVM 会创建临时的 Integer 对象,或者直接传递一个 int?
元素存储在原语 int[] 中,并且仅直接分配给原语 int,因此未经优化,意味着不必要的对象创建, 只是为了在几分之一秒内摧毁它。
public class Test {
private interface Array<E> {
E get(int index);
void set(int index, E element);
}
private static class GenericArray<E> implements Array<E> {
private final E[] elements;
@SuppressWarnings("unchecked")
public GenericArray(int capacity) {
this.elements = (E[]) new Object[capacity];
}
@Override
public E get(int index) {
return elements[index];
}
@Override
public void set(int index, E element) {
elements[index] = element;
}
}
private static class IntArray<E> implements Array<Integer> {
private final int[] elements; // primitive int array
public IntArray(int capacity) {
this.elements = new int[capacity];
}
@Override
public Integer get(int index) {
return elements[index];
}
@Override
public void set(int index, Integer element) {
elements[index] = element;
}
}
private static void scenario(Array<Integer> array) {
int element = 256;
array.set(16, element); // primitive int given
element = array.get(16); // converted directly to primitive int
System.out.println(element);
}
public static void main(String[] args) {
Array<Integer> genericArray = new GenericArray<>(64);
Array<Integer> primitiveArray = new IntArray<>(64);
scenario(genericArray);
scenario(primitiveArray);
}
}
„…does JVM skip a temporary generic conversion…“
没有。它没有。它做了 Boxing Conversion…
…
5.1.7 Boxing Conversion
Boxing conversion treats expressions of a primitive type as expressions of a corresponding reference type. Specifically, the following nine conversions are called the boxing conversions:
…
- From type int to type Integer
…
…还有一个Unboxing Conversion…
…
5.1.8. Unboxing Conversion
Unboxing conversion treats expressions of a reference type as expressions of a corresponding primitive type. Specifically, the following eight conversions are called the unboxing conversions:
…
- From type Integer to type int
…
参见Java Tutorial's Autoboxing and Unboxing trail。
„…will JVM create temporary Integer objects, or directly pass an int?…“
两者兼而有之。首先是前者,最后是后者。
„…unnecessary object creation, just to destroy it in a fraction of second…“
设计 Java 语言的 Oracle 架构师可能同意您的观点……
„…The problem with boxing is that it is [ad-hoc] and expensive; extensive work has gone on under the hood to address both of these concerns…“ — Brian Goetz, State of Valhalla, March 2020
他们已经为您的请求提供了解决方案……
„…for a way to use primitive collections with generic interfaces…“
…如果你有耐心…
„…We are trying to preserve room for specialized generics as a future feature… We wish to reserve a natural notation in the future for specialized types, such as
List<int>
…“ — Migration: specialized generics, Brian Goetz, State of Valhalla
专门的仿制药还没有出现。但是你可以 take other early-access features of Valhalla out for a spin today.
Java 没有基本类型的泛型 (yet)。
您的 IntArray
处理 Integer
对象,至少在字节码级别。如果我们反编译 class,我们会清楚地看到对装箱 Integer.valueOf
和拆箱 Integer.intValue
方法的调用:
javap -c -private Test$IntArray
public java.lang.Integer get(int);
Code:
0: aload_0
1: getfield #2 // Field elements:[I
4: iload_1
5: iaload
6: invokestatic #3 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: areturn
public void set(int, java.lang.Integer);
Code:
0: aload_0
1: getfield #2 // Field elements:[I
4: iload_1
5: aload_2
6: invokevirtual #4 // Method java/lang/Integer.intValue:()I
9: iastore
10: return
然而,JIT 编译器有一个优化来消除冗余 boxing-unboxing 对:-XX:+EliminateAutoBox
。默认情况下优化是开启的,但不幸的是并不总是有效。在 JMH 基准测试的帮助下,让我们看看它是否适用于您的情况。
package bench;
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class GenericArrays {
Array<Integer> genericArray = new GenericArray<>(64);
Array<Integer> primitiveArray = new IntArray(64);
int n;
@Setup
public void setup() {
for (int i = 0; i < 64; i++) {
genericArray.set(i, i + 256);
primitiveArray.set(i, i + 256);
}
}
@Benchmark
public int getGeneric() {
return genericArray.get(n++ & 63);
}
@Benchmark
public int getPrimitive() {
return primitiveArray.get(n++ & 63);
}
@Benchmark
@Fork(jvmArgsAppend = "-XX:-EliminateAutoBox")
public int getPrimitiveNoOpt() {
return primitiveArray.get(n++ & 63);
}
@Benchmark
public void setGeneric() {
genericArray.set(n++ & 63, n);
}
@Benchmark
public void setPrimitive() {
primitiveArray.set(n++ & 63, n);
}
@Benchmark
@Fork(jvmArgsAppend = "-XX:-EliminateAutoBox")
public void setPrimitiveNoOpt() {
primitiveArray.set(n++ & 63, n);
}
private interface Array<E> {
E get(int index);
void set(int index, E element);
}
static class GenericArray<E> implements Array<E> {
private final E[] elements;
@SuppressWarnings("unchecked")
public GenericArray(int capacity) {
this.elements = (E[]) new Object[capacity];
}
@Override
public E get(int index) {
return elements[index];
}
@Override
public void set(int index, E element) {
elements[index] = element;
}
}
static class IntArray implements Array<Integer> {
private final int[] elements;
public IntArray(int capacity) {
this.elements = new int[capacity];
}
@Override
public Integer get(int index) {
return elements[index];
}
@Override
public void set(int index, Integer element) {
elements[index] = element;
}
}
}
当 运行 在 JDK 14.0.2 上进行基准测试时,我得到以下分数(越低越好)。
Benchmark Mode Cnt Score Error Units
GenericArrays.getGeneric avgt 20 3,769 ± 0,039 ns/op
GenericArrays.getPrimitive avgt 20 3,445 ± 0,037 ns/op
GenericArrays.getPrimitiveNoOpt avgt 20 5,147 ± 0,073 ns/op
GenericArrays.setGeneric avgt 20 10,491 ± 0,055 ns/op
GenericArrays.setPrimitive avgt 20 3,896 ± 0,023 ns/op
GenericArrays.setPrimitiveNoOpt avgt 20 4,078 ± 0,077 ns/op
这使我们得出两个观察结果:
- 原始数组似乎表现更好;
EliminateAutoBox
优化显然有效,因为当优化关闭时,时间会更高。
现在让我们验证优化是否有助于避免不必要的分配。
JMH (-prof gc
) 中内置的 GC 分析器将完成这项工作。
Benchmark Mode Cnt Score Error Units
GenericArrays.getGeneric:·gc.alloc.rate.norm avgt 20 ≈ 10⁻⁵ B/op
GenericArrays.getPrimitive:·gc.alloc.rate.norm avgt 20 ≈ 10⁻⁵ B/op
GenericArrays.getPrimitiveNoOpt:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
GenericArrays.setGeneric:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
GenericArrays.setPrimitive:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
GenericArrays.setPrimitiveNoOpt:·gc.alloc.rate.norm avgt 20 16,000 ± 0,001 B/op
这里我们看到getPrimitive
基准的分配率为零。这意味着,JVM 能够消除临时 Integer
对象的分配。当优化关闭时,分配率预计为每次操作 16 字节 - 恰好是一个 Integer
对象的大小。
出于某种原因,JVM 无法消除 setPrimitive
中的装箱。正如我之前所说,优化是脆弱的,并非在所有情况下都有效。
但是,setPrimitive
仍然比 setGeneric
快很多。好处在于存储原语比存储引用更有效,因为存储引用通常需要 GC 屏障。