Общий метод для преобразования примитивных массивов - PullRequest
0 голосов
/ 24 июня 2018

У меня есть старый код с большим количеством методов, таких как long[] toLongArray(int[] array), но для многих различных конфигураций примитивных типов (с обеих сторон), и мне просто интересно, возможно ли сделать один универсальный метод для этого - без потери производительности.
Сначала я создал простой метод, используя MethodHandles для пары int [] -> long []:

static final MethodHandle getIntElement  = MethodHandles.arrayElementGetter(int[].class);
static final MethodHandle setLongElement = MethodHandles.arrayElementSetter(long[].class);
static long[] specializedMethodHandle(int[] array) throws Throwable {
    long[] newArray = new long[array.length];
    for (int i = 0; i < array.length; i++) getIntElement.invokeExact(newArray, i, (long) (int) setLongElement.invokeExact(array, i));
    return newArray;
}

И это прекрасно работает - такая же производительность, как и в ручном цикле, поэтому я решил сделать это универсальным:

static Map<Class<?>, MethodHandle> metHanGettersObj = Map.of(int[].class, MethodHandles.arrayElementGetter(int[].class).asType(MethodType.methodType(Object.class, Object.class, int.class)));
static Map<Class<?>, MethodHandle> metHanSettersObj = Map.of(long[].class, MethodHandles.arrayElementSetter(long[].class).asType(MethodType.methodType(void.class, Object.class, int.class, Object.class)));
static <F, T> T genericMethodHandleObject(Class<T> to, F array) throws Throwable {
    int length = Array.getLength(array);
    Object newArray = Array.newInstance(to.getComponentType(), length);
    MethodHandle getElement = metHanGettersObj.get(array.getClass());
    MethodHandle setElement = metHanSettersObj.get(to);
    for (int i = 0; i < length; i++) setElement.invokeExact(newArray, i, getElement.invokeExact(array, i));
    return (T) newArray;
}

Но это работает намного медленнее, для моего примера массива из 500000 элементов это было в 15 раз медленнее.
Что интересно, CompiledScript, созданный с помощью движка JavaScript Nashorn, работает примерно на 20% быстрее, чем этот код. (простой цикл копирования внутри)

Так мне интересно, знает ли кто-нибудь другой способ сделать это? Я, вероятно, не буду использовать это нигде, поскольку это начинает быть слишком «хакерским», но теперь мне просто нужно знать, возможно ли это вообще - поскольку ни один универсальный метод с дескрипторами метода не работает нормально, так почему этот метод такой медленный, и Можно ли сделать это быстрее?

1 Ответ

0 голосов
/ 24 июня 2018

Вы можете загрузить вместе дескриптор метода преобразователя массива, который затем кешируется в некоторой статической карте.

Вот эталонный тест, включая код.Метод convertBootstrap создает конвертер, и здесь происходит настоящее волшебство:

@BenchmarkMode({ Mode.AverageTime })
@Warmup(iterations = 10, batchSize = 1)
@Measurement(iterations = 10, batchSize = 1)
@Fork(1)
@State(Scope.Thread)
public class MyBenchmark {

    int[] input;

    static final Map<Class<?>, Map<Class<?>, Function<?, ?>>> cacheGeneric = new HashMap<>();

    @Setup
    public void setup() {
        input = new Random(1).ints().limit(500_000).toArray();
    }

    @Benchmark
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public long[] manual() {
        long[] result = new long[input.length];
        for(int i = 0 ; i < input.length; i++) {
            result[i] = input[i];
        }
        return result;
    }

    @Benchmark
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public long[] cachedGeneric() {
        return getWrapped(int[].class, long[].class).apply(input);
    }

    @Benchmark
    @OutputTimeUnit(TimeUnit.MILLISECONDS)
    public long[] reflective() throws Throwable {
        return genericMethodHandleObject(long[].class, input);
    }

    static Map<Class<?>, MethodHandle> metHanGettersObj = Map.of(int[].class, MethodHandles.arrayElementGetter(int[].class).asType(MethodType.methodType(Object.class, Object.class, int.class)));
    static Map<Class<?>, MethodHandle> metHanSettersObj = Map.of(long[].class, MethodHandles.arrayElementSetter(long[].class).asType(MethodType.methodType(void.class, Object.class, int.class, Object.class)));
    static <F, T> T genericMethodHandleObject(Class<T> to, F array) throws Throwable {
        int length = Array.getLength(array);
        Object newArray = Array.newInstance(to.getComponentType(), length);
        MethodHandle getElement = metHanGettersObj.get(array.getClass());
        MethodHandle setElement = metHanSettersObj.get(to);
        for (int i = 0; i < length; i++) setElement.invokeExact(newArray, i, getElement.invokeExact(array, i));
        return (T) newArray;
    }

    @SuppressWarnings("unchecked")
    public static <F, T> Function<F, T> getWrapped(Class<F> from, Class<T> to) {
        return (Function<F, T>) cacheGeneric.computeIfAbsent(from, k -> new HashMap<>())
            .computeIfAbsent(
                to, k -> {
                    MethodHandle mh = convertBootstrap(from, to);
                    return arr -> {
                        try {
                            return (T) mh.invoke(arr);
                        } catch (Throwable e) {
                            throw new RuntimeException(e);
                        }
                    };
                });
    }

    public static MethodHandle convertBootstrap(Class<?> from, Class<?> to) {       
        MethodHandle getter = arrayElementGetter(from);
        MethodHandle setter = arrayElementSetter(to);

        MethodHandle body = explicitCastArguments(setter, methodType(void.class, to, int.class, from.getComponentType()));      
        body = collectArguments(body, 2, getter); // get from 1 array, set in other
        body = permuteArguments(body, methodType(void.class, to, int.class, from), 0, 1, 2, 1);
        body = collectArguments(identity(to), 1, body); // create pass-through for first argument
        body = permuteArguments(body, methodType(to, to, int.class, from), 0, 0, 1, 2);

        MethodHandle lenGetter = arrayLength(from);
        MethodHandle cons = MethodHandles.arrayConstructor(to);
        MethodHandle init = collectArguments(cons, 0, lenGetter);

        MethodHandle loop = countedLoop(lenGetter, init, body);
        return loop;
    }
}

Результаты теста примерно одинаковы для моего метода и руководства (чем меньше балл, тем лучше):

# JMH version: 1.19
# VM version: JDK 10.0.1, VM 10.0.1+10

Benchmark                  Mode  Cnt   Score   Error  Units
MyBenchmark.cachedGeneric  avgt   10   1.175 ± 0.046  ms/op
MyBenchmark.manual         avgt   10   1.149 ± 0.098  ms/op
MyBenchmark.reflective     avgt   10  10.165 ± 0.665  ms/op

Я был действительно очень удивлен, насколько хорошо это оптимизируется (если я не ошибся где-то в тесте, но не могу найти его).Если вы увеличите количество элементов до 5 миллионов, вы снова сможете увидеть разницу:

Benchmark                  Mode  Cnt    Score    Error  Units
MyBenchmark.cachedGeneric  avgt   10  277.764 ± 14.217  ms/op
MyBenchmark.manual         avgt   10   14.851 ±  0.317  ms/op
MyBenchmark.reflective     avgt   10   76.599 ±  3.695  ms/op

Эти цифры говорят мне, что какой-то предел циклов разворачивания / вставки / чего-то еще достигается, посколькуразница внезапно становится намного больше.

Вероятно, вы также увидите снижение производительности, когда типы массивов статически не известны.

...