英文:
does JVM skip a temporary generic conversion
问题
我正在寻找一种在泛型接口中使用原始集合的方法。
对于 IntArray 类和 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);
}
}
英文:
I'm looking for a way to use primitive collections with generic interfaces.
In the case of IntArray class and scenario function, will JVM create temporary Integer objects, or directly pass an int?
Elements are stored in a primitive int[] and assigned only directly to primitive int so leaving this unoptimized, implies unnecessary object creation, just to destroy it in a fraction of second.
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);
}
}
答案1
得分: 3
Java目前还不支持原始类型的泛型(但有计划)。
你的 IntArray
处理的是 Integer
对象,至少在字节码级别上是这样的。如果我们对该类进行反编译,我们会清楚地看到对装箱方法 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 编译器有一种优化方式可以消除冗余的装箱-拆箱对:-XX:+EliminateAutoBox
。这个优化默认是开启的,但不幸的是并不总是生效。让我们看看在你的情况下是否通过 JMH 基准测试来验证它是否生效。
package bench;
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class GenericArrays {
// ...(省略其他代码)
@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);
}
// ...(省略其他代码)
}
在 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 中的 GC 分析器(-prof 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 屏障操作。
英文:
Java does not have generics over primitive types (yet).
Your IntArray
deals with Integer
objects, at least at the bytecode level. If we decompile the class, we'll clearly see calls to the boxing Integer.valueOf
and unboxing Integer.intValue
methods:
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
However, JIT compiler has an optimization to eliminate redundant boxing-unboxing pairs: -XX:+EliminateAutoBox
. The optimization is ON by default, but unfortunately does not always work. Let's see if it works in your case with the help of the JMH benchmark.
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;
}
}
}
When run the benchmark on JDK 14.0.2, I get the following scores (lower is better).
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
This leads us to two observations:
- Primitive array seems to perform better;
EliminateAutoBox
optimization apparently works, since when the optimization is off, the timings are higher.
Now let's verify if the optimization helps to avoid unnecessary allocations.
GC profiler built into JMH (-prof gc
) will do the job.
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
Here we see that the allocation rate of getPrimitive
benchmark is zero. This means, the JVM was able to eliminate allocation of a temporary Integer
object. When the optimization is off, the allocation rate is expectedly 16 bytes per operation - exactly the size of one Integer
object.
For some reason, JVM was not able to eliminate boxing in setPrimitive
. As I've told earlier, the optimization is fragile and does not work in all cases.
However, setPrimitive
is still a way faster than setGeneric
. The benefit comes from the fact that storing a primitive is more efficient than storing a reference, because storing a reference typically requires a GC barrier.
答案2
得分: -2
> „…JVM是否会跳过临时泛型转换?“
不会。它会执行装箱转换…
> …
>
> 5.1.7 装箱转换
>
> 装箱转换将原始类型的表达式视为相应引用类型的表达式。具体而言,以下九种转换称为装箱转换:
>
> …
> * 从 int 类型到 Integer 类型
>
> …
…还有拆箱转换…
> …
>
> 5.1.8. 拆箱转换
>
> 拆箱转换将引用类型的表达式视为相应原始类型的表达式。具体而言,以下八种转换称为拆箱转换:
>
> …
>
> * 从 Integer 类型到 int 类型
>
> …
> „…JVM是否会创建临时的 Integer 对象,还是直接传递 int?“
两者都会。首先是前者,然后最终是后者。
> „…不必要的对象创建,只为在短短几分之一秒内销毁它…“
设计 Java 语言的 Oracle 架构师们可能同意您的观点…
>> „…装箱的问题在于它是[临时的]并且昂贵;在幕后进行了大量的工作来解决这两个问题…“ — Brian Goetz,Valhalla 状态,2020 年 3 月
他们对您的请求有一个解决方案…
> „…寻找一种使用具有通用接口的原始集合的方法…“
…如果您有耐心的话…
>> „…我们正在努力保留专门用于将来功能的通用接口…我们希望将来保留自然的符号表示法,用于专门的类型,比如 List<int>
…“ — 迁移:专门的通用接口,Brian Goetz,Valhalla 状态
专门的通用接口尚不存在。但您可以今天就尝试 Valhalla 的其他早期访问功能。
英文:
> „…does JVM skip a temporary generic conversion…“
No. It doesn't. It does a 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
>
> …
…And an 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
>
> …
See the Java Tutorial's Autoboxing and Unboxing trail.
> „…will JVM create temporary Integer objects, or directly pass an int?…“
It does both. First the former followed finally by the latter.
> „…unnecessary object creation, just to destroy it in a fraction of second…“
The architects at Oracle that design the Java language probably agree with you…
>> „…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
And they have a solution in mind for your request…
> „…for a way to use primitive collections with generic interfaces…“
…If you're patient…
>> „…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
Specialized generics aren't a thing yet. But you can take other early-access features of Valhalla out for a spin today.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论