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 屏障。