java中是否有任何 native 方法可以将长数组复制/转换为字节数组,反之亦然。我知道以下方法
ByteBuffer bb = ByteBuffer.allocate(longArray.length * Long.BYTES);
bb.asLongBuffer().put(longArray);
return bb.array();
但是上述方法非常非常慢,尤其是当我们的 Java 应用程序处理大量数据时。
System.arraycopy 是复制相同类型数组的绝佳性能。如果java声称system.arraycopy正在使用 native c方法,那么为什么它们不包括像C memcpy那样将long/int数组复制到字节数组来完成这项工作。
感谢任何帮助。
谢谢。
最佳答案
ByteBuffer.asLongBuffer().put()
是在不同数组类型之间复制的正确方法。它很简单,纯Java,而且速度也不是那么慢。下面我就来演示一下。
请注意,要使结果与 memcpy
等效,您需要将 ByteBuffer
切换为 native 字节顺序。默认情况下,ByteBuffer 是 BIG_ENDIAN,而 x86 架构是 LITTLE_ENDIAN。切换到 native 字节顺序也将使复制速度更快。
bb.order(ByteOrder.nativeOrder());
还有一些值得一提的其他转换数组的方法。
Unsafe.copyMemory()
- JNI
GetPrimitiveArrayCritical
+SetByteArrayRegion
.
sun.misc.Unsafe
是 JDK 私有(private)、不受支持且已弃用的 API,但它仍然适用于所有版本,至少从 JDK 6 到 JDK 14。它的好处是它Java API - 无需创建 native 库。
相反,JNI 函数需要加载 native 库,但这些函数是标准且受支持的。 GetPrimitiveArrayCritical
+ SetByteArrayRegion
的组合允许将数据直接从一个数组复制到另一个数组,无需中间存储。
HotSpot JVM 还有一个未记录的扩展 - Critical Natives ,它允许直接从 native 代码访问 Java 原始数组,而无需 JNI 开销。但请记住,依赖未记录的 API 会使您的代码不可移植。好消息是 Critical Natives 与常规 native 方法兼容,即当您实现两者时,您可以确保代码在任何地方都可以工作。
性能怎么样?
我创建了一个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");
}
}
数组复制.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
ByteBuffer 为何比 Unsafe 更快?诀窍在于 JVM 编译器可以向量化、展开和内联 ByteBuffer 的复制循环,而 Unsafe.copyMemory 始终调用 JVM 运行时。
关于将长数组转换为字节数组的Java native 方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61844613/