将长数组转换为字节数组的 Java 本机方法



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());

还有其他一些方法可以转换数组值得一提。

  1. Unsafe.copyMemory()
  2. JNIGetPrimitiveArrayCritical+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运行时。

最新更新