java.lang.reflect.Array 的表现

Performance of java.lang.reflect.Array

由于我在项目中大量使用反射访问数组,所以我决定比较 array[index]java.lang.reflect.Array.get(array, index) 的性能。虽然我预计反射调用会慢很多,但我惊讶地发现它们慢了 10-16 倍。

所以我决定编写一个简单的实用方法,其功能与 Array#get 大致相同,但通过转换对象而不是使用本机方法接收给定索引处的数组(Array#get):

public static Object get(Object array, int index){
    Class<?> c = array.getClass();
    if (int[].class == c) {
        return ((int[])array)[index];
    } else if (float[].class == c) {
        return ((float[])array)[index];
    } else if (boolean[].class == c) {
        return ((boolean[])array)[index];
    } else if (char[].class == c) {
        return ((char[])array)[index];
    } else if (double[].class == c) {
        return ((double[])array)[index];
    } else if (long[].class == c) {
        return ((long[])array)[index];
    } else if (short[].class == c) {
        return ((short[])array)[index];
    } else if (byte[].class == c) {
        return ((byte[])array)[index];
    }
    return ((Object[])array)[index];
}

我相信此方法提供与 Array#get 相同的功能,但抛出的异常有显着差异(例如,抛出 ClassCastException 而不是 IllegalArgumentException,如果使用非数组的 Object 调用方法。)。

令我惊讶的是,这个实用方法比 Array#get 好。

三个问题:

  1. 这里的其他人是否遇到与 Array#get 相同的性能问题,或者这可能是 hardware/platform/Java-version 问题(我在双核 Windows 上使用 Java 8 进行了测试7 台笔记本电脑)?
  2. 我是否遗漏了有关方法 Array#get 功能的某些信息? IE。是否有某些功能必须使用本机调用才能实现?
  3. 为什么 Array#get 是使用本机方法实现的,而在纯 Java 中可以以更高的性能实现相同的功能,是否有特定原因?

测试类和结果

测试已使用 Caliper(编译代码所需的 git 的最新 Caliper)完成。但为了您的方便,我还包含了一个执行简化测试的主要方法(您需要删除 Caliper 注释才能使其编译)。

测试类:

import java.lang.reflect.Array;
import com.google.caliper.BeforeExperiment;
import com.google.caliper.Benchmark;

public class ArrayAtBenchmark {

    public static final class ArrayUtil {
        public static Object get(Object array, int index){
            Class<?> c = array.getClass();
            if (int[].class == c) {
                return ((int[])array)[index];
            } else if (float[].class == c) {
                return ((float[])array)[index];
            } else if (boolean[].class == c) {
                return ((boolean[])array)[index];
            } else if (char[].class == c) {
                return ((char[])array)[index];
            } else if (double[].class == c) {
                return ((double[])array)[index];
            } else if (long[].class == c) {
                return ((long[])array)[index];
            } else if (short[].class == c) {
                return ((short[])array)[index];
            } else if (byte[].class == c) {
                return ((byte[])array)[index];
            }
            return ((Object[])array)[index];
        }
    }

    private static final int ELEMENT_SIZE = 100;
    private Object[] objectArray;

    @BeforeExperiment
    public void setup(){
        objectArray = new Object[ELEMENT_SIZE];
        for (int i = 0; i < objectArray.length; i++) {
            objectArray[i] = new Object();
        }
    }

    @Benchmark
    public int ObjectArray_at(int reps){
        int dummy = 0;
        for (int i = 0; i < reps; i++) {
            for (int j = 0; j < ELEMENT_SIZE; j++) {
                dummy |= objectArray[j].hashCode();
            }
        }
        return dummy;
    }

    @Benchmark
    public int ObjectArray_Array_get(int reps){
        int dummy = 0;
        for (int i = 0; i < reps; i++) {
            for (int j = 0; j < ELEMENT_SIZE; j++) {
                dummy |= Array.get(objectArray, j).hashCode();
            }
        }
        return dummy;
    }

    @Benchmark
    public int ObjectArray_ArrayUtil_get(int reps){
        int dummy = 0;
        for (int i = 0; i < reps; i++) {
            for (int j = 0; j < ELEMENT_SIZE; j++) {
                dummy |= ArrayUtil.get(objectArray, j).hashCode();
            }
        }
        return dummy;
    }

    // test method to use without Cailper
    public static void main(String[] args) {
        ArrayAtBenchmark benchmark = new ArrayAtBenchmark();
        benchmark.setup();

        int warmup = 100000;
        // warm up 
        benchmark.ObjectArray_at(warmup);
        benchmark.ObjectArray_Array_get(warmup);
        benchmark.ObjectArray_ArrayUtil_get(warmup);

        int reps = 100000;

        long start = System.nanoTime();
        int temp = benchmark.ObjectArray_at(reps);
        long end = System.nanoTime();
        long time = (end-start)/reps;
        System.out.println("time for ObjectArray_at: " + time + " NS");

        start = System.nanoTime();
        temp |= benchmark.ObjectArray_Array_get(reps);
        end = System.nanoTime();
        time = (end-start)/reps;
        System.out.println("time for ObjectArray_Array_get: " + time + " NS");

        start = System.nanoTime();
        temp |= benchmark.ObjectArray_ArrayUtil_get(reps);
        end = System.nanoTime();
        time = (end-start)/reps;
        System.out.println("time for ObjectArray_ArrayUtil_get: " + time + " NS");
        if (temp == 0) {
            // sanity check to prevent JIT to optimize the test methods away
            System.out.println("result:" + result);
        }
    }
}

可以查看 Caliper 结果 here

简化的 main 方法的结果在我的机器上看起来像这样:

time for ObjectArray_at: 620 NS
time for ObjectArray_Array_get: 10525 NS
time for ObjectArray_ArrayUtil_get: 1287 NS

附加信息

是的,Array.get在OpenJDK / Oracle JDK中很慢,因为它是通过native方法实现的,没有经过JIT优化。

Array.get 成为本机没有特殊原因,除了从 JDK 的最早版本开始就是这样(当时 JVM 不是很好,根本没有 JIT) .此外,还有来自 GNU Classpath 的纯 Java compatible implementation of java.lang.reflect.Array

目前(从 JDK 8u45 开始)仅 Array.newInstance and Array.getLength are optimized (being JVM intrinsics). Looks like nobody really cared about performance of reflective get/set methods. But as @Marco13 there is an open issue JDK-8051447 可以在将来的某个时候改进 Array.* 方法的性能。