【问题标题】:Generic method to convert primitive arrays转换原始数组的通用方法
【发布时间】:2018-12-03 05:05:41
【问题描述】:

我的旧代码有很多方法,比如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 倍以上。
有趣的是,使用 Nashorn javascript 引擎制作的 CompiledScript 比这段代码快 20% 左右。 (里面有简单的复制循环)

所以我想知道是否有人知道其他方法可以做到这一点?我可能不会在任何地方使用它,因为它开始太“hacky”了,但现在我只需要知道它是否可能 - 因为没有带有方法句柄的通用方法可以正常工作,所以为什么这个方法这么慢,并且有没有可能让它更快?

【问题讨论】:

  • 您如何对此进行基准测试?
  • “可能为此创建一个通用方法”不,因为没有有用的通用超类型。您所做的任何事情都将是反射性的,并且其性能将低于单个方法。
  • @JornVernee 是的,否则我不知道它是否更慢。 (JMH 中的简单基准)
  • @AndyTurner MethodHandles 可以和普通代码一样快 - 但只有在并非在所有情况下都正确使用时,在某些地方 lambda 工厂 + 方法句柄会有所帮助。但到目前为止,我找不到更好的方法。但是,如果您将使用反射和Array 类来执行此操作,它将比此 MethodHandle 版本慢 15 倍 - 所以这已经比反射更好。因为方法句柄不仅仅是反射。
  • 不认真,您如何对此进行基准测试?

标签: java performance methodhandle


【解决方案1】:

您可以将数组转换器方法句柄引导在一起,然后将其缓存在一些静态映射中。

这是一个包含代码的基准。 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

我真的很惊讶它的优化程度(除非我在某处的基准测试中犯了错误,但我找不到它)。如果将元素数量增加到 500 万,您可以再次看到差异:

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

这些数字向我表明,一些循环展开/内联/其他限制正在受到影响,因为差异突然变大了。

当数组类型不是静态已知时,您可能还会看到性能下降。

【讨论】:

  • 您在 computeIfAbsent 中使用了 from 两次,否则它似乎可以工作 - 有趣的是热身 - 一开始它很慢,但随后它开始运行并且快了 80 倍。我需要检查一下更“动态”的内容,以检查它是否会因使用更多不同的类型而中断。
  • @GotoFinal 哎呀,是的,这是一个错字。我还针对您的解决方案进行了基准测试,发现它比我在这里展示的两种方法慢了大约 8 倍。我想这也取决于机器/JDK 的效果如何。是的,方法句柄的快速路径非常快,他们给予了很多关注。恕我直言,最大的缺点是将它们放在一起(使用木棍建造森林)非常困难。
  • 奇怪,你能链接整个基准吗?也许它太短了?
  • 更奇怪的是 - 对于某些基准测试,您的方法似乎比正常循环更快(但预热时间很长)
  • @GotoFinal 我已经添加了您的解决方案的基准。
猜你喜欢
  • 1970-01-01
  • 2014-10-20
  • 2017-07-05
  • 2017-02-03
  • 1970-01-01
  • 1970-01-01
  • 2011-02-13
  • 2020-09-07
  • 1970-01-01
相关资源
最近更新 更多