Java.lang.reflect.Array的性能表现

16

由于我在项目中大量使用反射访问数组,所以我决定比较 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相同的功能,不同之处在于所抛出的异常(例如,如果使用不是数组的Object调用该方法,会抛出ClassCastException而不是IllegalArgumentException)。

令我惊讶的是,这种实用程序方法的性能比Array#get要好得多。

  1. 其他人是否也遇到了使用Array#get时的性能问题,或者这可能是硬件/平台/Java版本问题(我在双核Windows 7笔记本电脑上测试了Java 8)?
  2. 我是否错过了关于Array#get方法功能的某些内容?例如,是否有必须使用本机调用实现的一些功能?
  3. 为什么使用本机方法实现Array#get,而可以使用纯Java实现具有更高性能的相同功能?

测试类和结果

使用 Caliper 进行测试(需要最新的Caliper从git进行编译)。但是,为了您的方便,我还包括了执行简化测试的主函数(您需要删除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的结果可以在这里查看。

在我的机器上,简化后的主方法的结果如下:

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

附加信息


你有用多维数组测试过你的代码吗? - Aaron Digulla
只是好奇:你为什么要使用反射?我的猜测是即时编译器非常擅长优化“原始”数组访问;而任何涉及“反射”的操作都不会被 JIT 很好地优化。但这只是猜测... - GhostCat
@AaronDigulla 是的,我做了 - 结果是,多维数组是 Object[] 的实例。 - Balder
@Jägermeister 嗯,最终它“看起来”与上面的实用方法非常相似 - 你得到一个可以是Object[]或原始数组的对象,然后相应地处理该数组... - Balder
5
请见链接 https://bugs.openjdk.java.net/browse/JDK-8051447,同时查看`Array.c`的本地实现:“TODO: Performance”;-) - Marco13
显示剩余8条评论
1个回答

10

是的,在OpenJDK / Oracle JDK中,Array.get很慢,因为它由本地方法实现,并且没有被JIT优化。

Array.get没有特别的原因是本地的,除了在JDK的最早版本中(当JVM不太好并且根本没有JIT时)就已经这样了。此外,还有一个纯Java的兼容实现来自GNU Classpath的java.lang.reflect.Array

目前(截至JDK 8u45),只有Array.newInstanceArray.getLength被优化(作为JVM内置函数)。似乎没有人真正关心反射get/set方法的性能。但正如@Marco13 所指出的,有一个开放的问题JDK-8051447,在未来的某个时候改善Array.*方法的性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接