转换原始数组的通用方法
Generic method to convert primitive arrays
我有很多方法的旧代码,比如 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" 但现在我只需要知道它是否可能 - 因为没有带有方法句柄的通用方法可以正常工作,所以为什么这个这么慢, 是否可以让它更快?
您可以 bootstrap 将一个数组转换器方法句柄放在一起,然后将其缓存在一些静态映射中。
这是一个包含代码的基准测试。 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
这些数字向我表明,一些循环 un-rolling/inlining/something-else 限制正在被击中,因为差异突然变大了。
当数组类型不是静态已知时,您可能还会看到性能下降。
我有很多方法的旧代码,比如 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" 但现在我只需要知道它是否可能 - 因为没有带有方法句柄的通用方法可以正常工作,所以为什么这个这么慢, 是否可以让它更快?
您可以 bootstrap 将一个数组转换器方法句柄放在一起,然后将其缓存在一些静态映射中。
这是一个包含代码的基准测试。 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
这些数字向我表明,一些循环 un-rolling/inlining/something-else 限制正在被击中,因为差异突然变大了。
当数组类型不是静态已知时,您可能还会看到性能下降。