Java中是否有任何本机方法可以将长数组复制/转换为字节数组,反之亦然。我知道以下方法
ByteBuffer bb = ByteBuffer.allocate(longArray.length * Long.BYTES);
bb.asLongBuffer().put(longArray);
return bb.array();
但是上述方法非常非常慢,尤其是当我们的 Java 应用程序处理大量数据时。
System.arraycopy是复制相同类型数组的精彩表现。 如果Java声称System.arraycopy使用的是本机C方法,那么为什么它们不包括将long/int数组复制到字节数组,就像在C memcpy中那样做这项工作。
感谢任何帮助。
谢谢。
ByteBuffer.asLongBuffer().put()
是在不同数组类型之间进行复制的正确方法。它很简单,纯粹的Java,而且没有那么慢。我将在下面演示。
请注意,要使结果等效于memcpy
,您需要ByteBuffer
切换到本机字节顺序。默认情况下,字节缓冲区BIG_ENDIAN,而 x86 体系结构LITTLE_ENDIAN。切换到本机字节顺序也将使复制速度更快。
bb.order(ByteOrder.nativeOrder());
还有其他一些方法可以转换数组值得一提。
Unsafe.copyMemory()
- JNI
GetPrimitiveArrayCritical
+SetByteArrayRegion
.
sun.misc.Unsafe
是一个 JDK 私有、不受支持和已弃用的 API,但它仍然适用于所有版本,至少从 JDK 6 到 JDK 14。它的好处是它是Java API - 不需要制作本机库。
相反,JNI 函数需要加载本机库,但这些函数是标准且受支持的。GetPrimitiveArrayCritical
+SetByteArrayRegion
的组合允许将数据从一个阵列直接复制到另一个阵列,而无需中间存储。
HotSpot JVM还有一个未记录的扩展 - Critical Natives,它允许直接从本机代码访问Java原始数组,而无需JNI开销。但请记住,依赖未记录的 API 会使您的代码不可移植。好消息是,关键本机与常规本机方法兼容,即当您实现两者时,您可以确定代码将在任何地方工作。
性能如何?
我创建了一个JMH基准来比较所有讨论的技术。
package bench;
import org.openjdk.jmh.annotations.*;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@State(Scope.Benchmark)
public class LongArrayCopy {
@Param({"100", "1000", "10000"})
private int size;
private long[] longArray;
@Setup
public void setup() {
longArray = new long[size];
}
@Benchmark
public byte[] byteBuffer() {
ByteBuffer bb = ByteBuffer.allocate(longArray.length * Long.BYTES);
bb.order(ByteOrder.nativeOrder());
bb.asLongBuffer().put(longArray);
return bb.array();
}
@Benchmark
public byte[] jni() {
byte[] byteArray = new byte[longArray.length * Long.BYTES];
copy(longArray, byteArray, byteArray.length);
return byteArray;
}
@Benchmark
public byte[] jniCritical() {
byte[] byteArray = new byte[longArray.length * Long.BYTES];
copyCritical(longArray, byteArray, byteArray.length);
return byteArray;
}
@Benchmark
public byte[] unsafe() {
byte[] byteArray = new byte[longArray.length * Long.BYTES];
theUnsafe.copyMemory(longArray, Unsafe.ARRAY_LONG_BASE_OFFSET,
byteArray, Unsafe.ARRAY_BYTE_BASE_OFFSET,
byteArray.length);
return byteArray;
}
private static native void copy(long[] src, byte[] dst, int size);
private static native void copyCritical(long[] src, byte[] dst, int size);
private static final Unsafe theUnsafe;
static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
theUnsafe = (Unsafe) f.get(null);
} catch (Exception e) {
throw new RuntimeException(e);
}
System.loadLibrary("arraycopy");
}
}
arraycopy.c
#include <jni.h>
#include <string.h>
JNIEXPORT void Java_bench_LongArrayCopy_copy(JNIEnv* env, jobject unused,
jlongArray src, jbyteArray dst, jint size) {
void* data = (*env)->GetPrimitiveArrayCritical(env, src, NULL);
(*env)->SetByteArrayRegion(env, dst, 0, size, (jbyte*)data);
(*env)->ReleasePrimitiveArrayCritical(env, src, data, JNI_COMMIT);
}
JNIEXPORT void Java_bench_LongArrayCopy_copyCritical(JNIEnv* env, jobject unused,
jlongArray src, jbyteArray dst,
jint size) {
Java_bench_LongArrayCopy_copy(env, unused, src, dst, size);
}
JNIEXPORT void JavaCritical_bench_LongArrayCopy_copyCritical(jint srclen, jlong* src,
jint dstlen, jbyte* dst,
jint size) {
memcpy(dst, src, size);
}
JDK 8u221 上的结果(每复制 1000 个长整型数组的纳秒(:
Benchmark (size) Mode Cnt Score Error Units
LongArrayCopy.byteBuffer 1000 avgt 10 3204,239 ± 49,300 ns/op
LongArrayCopy.jni 1000 avgt 10 774,466 ± 2,973 ns/op
LongArrayCopy.jniCritical 1000 avgt 10 545,801 ± 3,643 ns/op
LongArrayCopy.unsafe 1000 avgt 10 552,265 ± 4,212 ns/op
与其他方法相比,ByteBuffer可能看起来慢得多。但是,自JDK 9以来,ByteBuffer性能得到了大量优化。如果我们在现代JDK(11或14(上运行相同的示例,我们将看到ByteBuffer实际上是最快的方法!
JDK 14.0.1
Benchmark (size) Mode Cnt Score Error Units
LongArrayCopy.byteBuffer 1000 avgt 10 566,038 ± 1,010 ns/op
LongArrayCopy.jni 1000 avgt 10 659,575 ± 2,145 ns/op
LongArrayCopy.jniCritical 1000 avgt 10 575,381 ± 2,283 ns/op
LongArrayCopy.unsafe 1000 avgt 10 602,838 ± 4,587 ns/op
字节缓冲区怎么可能比不安全更快?诀窍在于JVM编译器可以矢量化,展开和内联ByteBuffer的复制循环,而Unsafe.copyMemory
始终调用JVM运行时。