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

标签 java performance java-io

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

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

  1. Unsafe.copyMemory()
  2. 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/

相关文章:

php - MYSQL读取一个数据库会花很长时间吗?

c - 与单进程场景​​相比,多进程场景中的访问时间意外减少

c - 优化流程重新启动

java - 获取 java.net.SocketTimeoutException : Connection timed out in android

java - 文本文件不断删除文件上的内容。我该如何解决这个问题?

java - 使用 Spring Security 后 $http 请求给出 403?

java - 如何在 Play 模板中创建新对象/如何在 Play 模板中导入类

java - 我的 ImageView 的 Android 动画有什么问题?

java - 如何允许 Java 应用程序和用户前端 Web 界面之间的交互

java - 预计算文件流校验和