JVM是否跳过临时的通用转换

问题描述

我正在寻找一种通过通用接口使用原始集合的方法。

对于 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);
    }
}

解决方法

Java没有原始类型(yet)的泛型。

您的at java.net.DualStackPlainSocketImpl.connect0(Native Method) at java.net.DualStackPlainSocketImpl.socketConnect(Unknown Source) at java.net.AbstractPlainSocketImpl.doConnect(Unknown Source) at java.net.AbstractPlainSocketImpl.connectToAddress(Unknown Source) at java.net.AbstractPlainSocketImpl.connect(Unknown Source) at java.net.PlainSocketImpl.connect(Unknown Source) at java.net.SocksSocketImpl.connect(Unknown Source) at java.net.Socket.connect(Unknown Source) at com.mysql.cj.protocol.StandardSocketFactory.connect(StandardSocketFactory.java:155) at com.mysql.cj.protocol.a.NativeSocketConnection.connect(NativeSocketConnection.java:65) ... 16 more 处理IntArray对象,至少在字节码级别。如果我们反编译该类,则会清楚地看到对装箱Integer和拆箱Integer.valueOf方法的调用:

Integer.intValue
javap -c -private Test$IntArray

但是,JIT编译器进行了一项优化,以消除冗余的装箱对装箱对: 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 。该优化默认情况下为“开”,但是不幸的是并不总是有效。在JMH基准测试的帮助下,看看它是否对您有用。

-XX:+EliminateAutoBox

在JDK 14.0.2上运行基准测试时,我得到以下分数(越低越好)。

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;
        }
    }
}

这导致我们有两个发现:

  • 原始数组似乎表现更好;
  • 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 优化显然可行,因为关闭优化后,计时会更高。

现在让我们验证优化是否有助于避免不必要的分配。
内置到JMH(EliminateAutoBox)中的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,001 B/op GenericArrays.setPrimitive:·gc.alloc.rate.norm avgt 20 16,001 B/op GenericArrays.setPrimitiveNoOpt:·gc.alloc.rate.norm avgt 20 16,001 B/op 基准的分配率为零。这意味着,JVM能够消除对临时getPrimitive对象的分配。取消优化后,每次操作的分配速率预计为16个字节-恰好是一个Integer对象的大小。

由于某些原因,JVM无法消除Integer中的装箱。如前所述,该优化非常脆弱,并且并非在所有情况下都有效。

但是,setPrimitive仍然比setPrimitive快。这样做的好处是,存储原语比存储引用更有效,因为存储引用通常需要GC屏障。

,

…JVM是否跳过了临时的通用转换…

不。没有。它会执行Boxing Conversion

...

5.1.7装箱转换

装箱转换将原始类型的表达式视为相应引用类型的表达式。具体来说,以下九种转换称为装箱转换:

...

  • 从int类型到Integer类型

...

...还有一个Unboxing Conversion ...

...

5.1.8。取消装箱转换

拆箱转换将引用类型的表达式视为相应原始类型的表达式。具体来说,以下八种转换称为拆箱转换:

...

  • 从Integer类型到int类型

...

请参见Java Tutorial's Autoboxing and Unboxing trail

………JVM将创建临时的 Integer 对象,还是直接传递 int ?…

两者都做。首先是前者,然后是后者。

…不必要的对象创建,只需要在不到一秒钟的时间内破坏它……

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

………拳击的问题是[即席]和昂贵;为解决这两个问题,我们进行了广泛的工作……” — — Brian Goetz,State of Valhalla,March 2020

他们会为您的请求提供解决方案……

…有关通过通用接口使用原始集合的方法…

...如果你有耐心...

…我们正在努力为特殊泛型保留空间,作为将来的功能……我们希望将来为List<int>这样的特殊类型保留自然的符号。 Migration: specialized generics,Brian Goetz,State of Valhalla

专用泛型还不是什么。但是您可以take other early-access features of Valhalla out for a spin today

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...