【问题标题】:Compare Direct and Non-Direct ByteBuffer get/put operations比较直接和非直接 ByteBuffer 的 get/put 操作
【发布时间】:2012-06-25 19:36:26
【问题描述】:

从非直接字节缓冲区获取/放置是否比从直接字节缓冲区获取/放置更快?

如果我必须从直接字节缓冲区读取/写入,最好先读取/写入线程本地字节数组,然后使用字节数组完全更新(用于写入)直接字节缓冲区?

【问题讨论】:

    标签: java memory nio bytebuffer


    【解决方案1】:

    直接缓冲区保存 JNI 中的数据,因此 get() 和 put() 必须跨越 JNI 边界。非直接缓冲区保存 JVM 中的数据。

    所以:

    1. 如果您根本不使用 Java 领域的数据,例如只是将一个通道复制到另一个通道,直接缓冲区更快,因为数据根本不必跨越 JNI 边界。

    2. 相反,如果您在 Java 领域中处理数据,非直接缓冲区会更快。它是否显着取决于有多少数据必须跨越 JNI 边界以及每次传输的量子量。例如,一次从直接缓冲区获取或放入一个字节可能会非常昂贵,而一次获取/放入 16384 个字节将大大摊销 JNI 边界成本。

    要回答您的第二段,我将使用本地 byte[] 数组,而不是线程本地,但是如果我在 Java 领域中处理数据,我根本不会使用直接字节缓冲区。正如 Javadoc 所说,直接字节缓冲区应该只用于能够带来可衡量的性能优势的地方。

    【讨论】:

    • 谢谢,我的消息大小通常是 256 字节,我想写入套接字,我正在考虑将字节编码为线程本地字节 [] 数组,然后将字节数组复制到直接字节缓冲区并将直接字节缓冲区传递给套接字通道进行写入。
    • 直接字节缓冲区被池化。这会更好还是您建议将消息​​直接编码到直接字节缓冲区而不是使用临时字节数组
    • @user882659 见编辑。在这种情况下,使用直接缓冲区根本没有任何好处。
    • @EJP - 我花了几分钟查看 Java 7 源代码,但我看不到 JNI 调用在直接缓冲区上的 getput 的位置。您能指出它在代码中的哪个位置执行此操作吗?
    • @StephenC 请参阅 Javadoc。考虑到它们的描述和目的,直接缓冲区执行 JNI 调用是不可能的。
    【解决方案2】:

    从非直接字节缓冲区获取/放置是否比从直接字节缓冲区获取/放置更快?

    如果您将堆缓冲区与不使用本机字节顺序的直接缓冲区进行比较(大多数系统是小端,直接字节缓冲区的默认值是大端),性能非常相似。

    如果您使用本机有序字节缓冲区,则多字节值的性能会显着提高。对于byte,无论你做什么都没什么区别。

    在 HotSpot/OpenJDK 中,ByteBuffer 使用 Unsafe 类,许多native 方法被视为intrinsics。这是依赖于 JVM 的,并且 AFAIK Android VM 在最近的版本中将其视为固有的。

    如果您转储生成的程序集,您可以看到 Unsafe 中的内在函数被转换为一条机器代码指令。即它们没有 JNI 调用的开销。

    事实上,如果您进行微调,您可能会发现 ByteBuffer getXxxx 或 setXxxx 的大部分时间都花在边界检查上,而不是实际的内存访问。出于这个原因,我仍然在必要时直接使用 Unsafe 以获得最佳性能(注意:Oracle 不鼓励这样做)

    如果我必须从直接字节缓冲区读取/写入,最好先读取/写入线程本地字节数组,然后使用字节数组完全更新(用于写入)直接字节缓冲区?

    我不想看到这比什么更好。 ;) 这听起来很复杂。

    通常最简单的解决方案更好更快。


    您可以使用此代码自行测试。

    public static void main(String... args) {
        ByteBuffer bb1 = ByteBuffer.allocateDirect(256 * 1024).order(ByteOrder.nativeOrder());
        ByteBuffer bb2 = ByteBuffer.allocateDirect(256 * 1024).order(ByteOrder.nativeOrder());
        for (int i = 0; i < 10; i++)
            runTest(bb1, bb2);
    }
    
    private static void runTest(ByteBuffer bb1, ByteBuffer bb2) {
        bb1.clear();
        bb2.clear();
        long start = System.nanoTime();
        int count = 0;
        while (bb2.remaining() > 0)
            bb2.putInt(bb1.getInt());
        long time = System.nanoTime() - start;
        int operations = bb1.capacity() / 4 * 2;
        System.out.printf("Each putInt/getInt took an average of %.1f ns%n", (double) time / operations);
    }
    

    打印

    Each putInt/getInt took an average of 83.9 ns
    Each putInt/getInt took an average of 1.4 ns
    Each putInt/getInt took an average of 34.7 ns
    Each putInt/getInt took an average of 1.3 ns
    Each putInt/getInt took an average of 1.2 ns
    Each putInt/getInt took an average of 1.3 ns
    Each putInt/getInt took an average of 1.2 ns
    Each putInt/getInt took an average of 1.2 ns
    Each putInt/getInt took an average of 1.2 ns
    Each putInt/getInt took an average of 1.2 ns
    

    我很确定 JNI 调用需要的时间超过 1.2 ns。


    为了证明它不是“JNI”调用,而是它周围的胡言乱语导致了延迟。您可以直接使用 Unsafe 编写相同的循环。

    public static void main(String... args) {
        ByteBuffer bb1 = ByteBuffer.allocateDirect(256 * 1024).order(ByteOrder.nativeOrder());
        ByteBuffer bb2 = ByteBuffer.allocateDirect(256 * 1024).order(ByteOrder.nativeOrder());
        for (int i = 0; i < 10; i++)
            runTest(bb1, bb2);
    }
    
    private static void runTest(ByteBuffer bb1, ByteBuffer bb2) {
        Unsafe unsafe = getTheUnsafe();
        long start = System.nanoTime();
        long addr1 = ((DirectBuffer) bb1).address();
        long addr2 = ((DirectBuffer) bb2).address();
        for (int i = 0, len = Math.min(bb1.capacity(), bb2.capacity()); i < len; i += 4)
            unsafe.putInt(addr1 + i, unsafe.getInt(addr2 + i));
        long time = System.nanoTime() - start;
        int operations = bb1.capacity() / 4 * 2;
        System.out.printf("Each putInt/getInt took an average of %.1f ns%n", (double) time / operations);
    }
    
    public static Unsafe getTheUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
    

    打印

    Each putInt/getInt took an average of 40.4 ns
    Each putInt/getInt took an average of 44.4 ns
    Each putInt/getInt took an average of 0.4 ns
    Each putInt/getInt took an average of 0.3 ns
    Each putInt/getInt took an average of 0.3 ns
    Each putInt/getInt took an average of 0.3 ns
    Each putInt/getInt took an average of 0.3 ns
    Each putInt/getInt took an average of 0.3 ns
    Each putInt/getInt took an average of 0.3 ns
    Each putInt/getInt took an average of 0.3 ns
    

    因此,您可以看到native 调用比您预期的 JNI 调用要快得多。这种延迟的主要原因可能是 L2 缓存速度。 ;)

    全部在 i3 3.3 GHz 上运行

    【讨论】:

    • 谢谢彼得。这是非常有用的。顺便说一句,为什么 oracle 建议不要直接使用 Unsafe 。如果我们在生产代码中使用它可能会出现什么陷阱?
    • 顾名思义,它使用起来不安全,出现错误会导致系统崩溃。即它更快,因为所有保护都关闭了。我所做的是有两种实现,一种按原样使用 ByteBuffers,另一种使用 Unsafe。当我对软件的测试有信心并且我需要它时,您可以放入不安全的版本。
    • 事实上我已经使用 Unsafe 故意使系统崩溃,例如我想测试如果应用程序崩溃 here 会发生什么;)
    • @Bober02 没有任何保证。很大程度上取决于您测试的内容,甚至您使用的系统。在一台机器上进行测试时,我在堆上的速度是原来的两倍,而在另一个堆上的系统上速度是原来的 5 倍。您可以说的是,堆外可以减少垃圾,具体取决于您的操作方式。
    • 关于 Android 没有内在函数的部分是不正确的。懒得去搜索 Dalvik 源码,但至少在 ART 上,大多数直接 Buffers 的 get/put 方法委托给内在函数(准确地说,它们使用内部 Memory 类,而后者又是 implemented via intrinsics
    猜你喜欢
    • 1970-01-01
    • 2018-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多