Производительность java.lang.reflect.Array

Поскольку я активно использую рефлексивный доступ к массивам в проекте, я решил сравнить производительность array[index] vs 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.

Три вопроса:

  • У других есть те же проблемы с производительностью с Array#get, или это, возможно, проблема с оборудованием/платформой/Java-версией (я тестировал с Java 8 на двухъядерном ноутбуке Windows 7)?
  • Я пропустил что-то относительно функциональности метода Array#get? То есть есть ли какая-то функциональность, которая обязательно должна быть реализована с использованием собственного вызова?
  • Есть ли конкретная причина, почему Array#get был реализован с использованием собственных методов, когда одна и та же функциональность могла быть реализована в чистой Java с гораздо более высокой производительностью?

Тест-классы и результаты

Тесты были выполнены с использованием Caliper (последний калибр от git, необходимый для компиляции кода). Но для вашего удобства я также включил основной метод, который выполняет упрощенный тест (вам нужно удалить аннотации суппорта, чтобы скомпилировать его).

TestClass:

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

Результаты калибровки можно просмотреть здесь.

Результаты упрощенного основного метода выглядят так на моей машине:

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

Дополнительная информация

  • Результаты аналогичны при запуске JVM с "-сервером"
  • Другие методы Array (например, Array#getInt, Array#getLength, Array#set и т.д.) также работают намного медленнее, чем аналогично реализованные методы утилиты
  • Этот вопрос несколько связан с: Какова цель java.lang.reflect.Array в методах getter и setter?

Ответы

Ответ 1

Да, Array.get медленнее в OpenJDK/Oracle JDK, потому что он реализован нативным методом и не оптимизирован JIT.

Нет никакой особой причины, чтобы Array.get был родным, за исключением того, что это было так из самых ранних выпусков JDK (когда JVM был не очень хорош, и вообще не было JIT). Кроме того, существует чистая Java совместимая реализация java.lang.reflect.Array из класса GNU Class.

В настоящее время (с JDK 8u45) только Array.newInstance и Array.getLength оптимизированы (являются встроенными JVM). Похоже, никто не заботился о производительности рефлексивных методов get/set. Но поскольку @Marco13 заметил, есть открытая проблема JDK-8051447, чтобы улучшить производительность методов Array.* в будущем.