JVM会跳过临时泛型转换。

huangapple go评论68阅读模式
英文:

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&lt;E&gt; {
E get(int index);
void set(int index, E element);
}
private static class GenericArray&lt;E&gt; implements Array&lt;E&gt; {
private final E[] elements;
@SuppressWarnings(&quot;unchecked&quot;)
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&lt;E&gt; implements Array&lt;Integer&gt; {
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&lt;Integer&gt; 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&lt;Integer&gt; genericArray   = new GenericArray&lt;&gt;(64);
Array&lt;Integer&gt; primitiveArray = new IntArray&lt;&gt;(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&lt;Integer&gt; genericArray = new GenericArray&lt;&gt;(64);
Array&lt;Integer&gt; primitiveArray = new IntArray(64);
int n;
@Setup
public void setup() {
for (int i = 0; i &lt; 64; i++) {
genericArray.set(i, i + 256);
primitiveArray.set(i, i + 256);
}
}
@Benchmark
public int getGeneric() {
return genericArray.get(n++ &amp; 63);
}
@Benchmark
public int getPrimitive() {
return primitiveArray.get(n++ &amp; 63);
}
@Benchmark
@Fork(jvmArgsAppend = &quot;-XX:-EliminateAutoBox&quot;)
public int getPrimitiveNoOpt() {
return primitiveArray.get(n++ &amp; 63);
}
@Benchmark
public void setGeneric() {
genericArray.set(n++ &amp; 63, n);
}
@Benchmark
public void setPrimitive() {
primitiveArray.set(n++ &amp; 63, n);
}
@Benchmark
@Fork(jvmArgsAppend = &quot;-XX:-EliminateAutoBox&quot;)
public void setPrimitiveNoOpt() {
primitiveArray.set(n++ &amp; 63, n);
}
private interface Array&lt;E&gt; {
E get(int index);
void set(int index, E element);
}
static class GenericArray&lt;E&gt; implements Array&lt;E&gt; {
private final E[] elements;
@SuppressWarnings(&quot;unchecked&quot;)
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&lt;Integer&gt; {
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 &#177;   0,039   ns/op
GenericArrays.getPrimitive       avgt   20     3,445 &#177;   0,037   ns/op
GenericArrays.getPrimitiveNoOpt  avgt   20     5,147 &#177;   0,073   ns/op
GenericArrays.setGeneric         avgt   20    10,491 &#177;   0,055   ns/op
GenericArrays.setPrimitive       avgt   20     3,896 &#177;   0,023   ns/op
GenericArrays.setPrimitiveNoOpt  avgt   20     4,078 &#177;   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:&#183;gc.alloc.rate.norm         avgt   20    ≈ 10⁻⁵              B/op
GenericArrays.getPrimitive:&#183;gc.alloc.rate.norm       avgt   20    ≈ 10⁻⁵              B/op
GenericArrays.getPrimitiveNoOpt:&#183;gc.alloc.rate.norm  avgt   20    16,000 &#177;   0,001    B/op
GenericArrays.setGeneric:&#183;gc.alloc.rate.norm         avgt   20    16,000 &#177;   0,001    B/op
GenericArrays.setPrimitive:&#183;gc.alloc.rate.norm       avgt   20    16,000 &#177;   0,001    B/op
GenericArrays.setPrimitiveNoOpt:&#183;gc.alloc.rate.norm  avgt   20    16,000 &#177;   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 类型
>
> …

请参阅Java 教程中的自动装箱和拆箱指南

> „…JVM是否会创建临时的 Integer 对象,还是直接传递 int

两者都会。首先是前者,然后最终是后者。

> „…不必要的对象创建,只为在短短几分之一秒内销毁它…

设计 Java 语言的 Oracle 架构师们可能同意您的观点…

>> „…装箱的问题在于它是[临时的]并且昂贵;在幕后进行了大量的工作来解决这两个问题…“ — Brian Goetz,Valhalla 状态,2020 年 3 月

他们对您的请求有一个解决方案…

> „…寻找一种使用具有通用接口的原始集合的方法…

…如果您有耐心的话…

>> „…我们正在努力保留专门用于将来功能的通用接口…我们希望将来保留自然的符号表示法,用于专门的类型,比如 List&lt;int&gt;“ — 迁移:专门的通用接口,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&lt;int&gt;“ — 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.

huangapple
  • 本文由 发表于 2020年9月19日 21:30:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/63969366.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定