ByteBuffer.allocate()与ByteBuffer.allocateDirect()


144

allocate()或到allocateDirect(),这就是问题。

几年以来,我一直坚持认为,由于DirectByteBuffers是操作系统级别的直接内存映射,因此与get相比,它在执行get / put调用时将执行得更快HeapByteBuffer。到目前为止,我从来没有真正想知道有关该情况的确切细节。我想知道这两种类型中ByteBuffer的哪种更快,以及在什么条件下。


要给出具体答案,您需要具体说明您对他们的看法。如果一个总是比另一个快,那为什么会有两个变种。也许您可以进一步解释为什么现在“真的很想找出确切的细节” BTW:您是否阅读过代码,尤其是DirectByteBuffer?
彼得·劳瑞

它们将用于读取和写入SocketChannel配置为非阻塞的。因此,对于@bmargulies所说的内容,DirectByteBuffers将在通道上更快地执行。

@Gnarly至少我的答案的当前版本说,渠道有望受益。
bmargulies

Answers:


150

罗恩·希切斯(Ron Hitches)在其出色的著作《Java NIO》中似乎提供了我认为可以很好地回答您的问题的答案:

操作系统在内存区域上执行I / O操作。就操作系统而言,这些存储区是连续的字节序列。不足为奇的是,只有字节缓冲区才有资格参与I / O操作。还记得操作系统将直接访问该进程的地址空间(在本例中为JVM进程)以传输数据。这意味着作为I / O操作目标的存储区必须是连续的字节序列。在JVM中,字节数组可能不会连续存储在内存中,否则垃圾收集器可以随时移动它。数组是Java中的对象,数据在该对象中的存储方式可能因一个JVM实现而异。

因此,引入了直接缓冲区的概念。直接缓冲区旨在与通道和本机I / O例程进行交互。他们尽最大努力将字节元素存储在通道可用于直接访问或原始访问的存储区中,方法是使用本机代码告诉操作系统直接耗尽或填充存储区。

直接字节缓冲区通常是I / O操作的最佳选择。根据设计,它们支持JVM可用的最有效的I / O机制。非直接字节缓冲区可以传递给通道,但是这样做可能会导致性能下降。非直接缓冲区通常不可能成为本机I / O操作的目标。如果将非直接ByteBuffer对象传递给通道进行写入,则该通道可能会在每次调用时隐式执行以下操作:

  1. 创建一个临时直接ByteBuffer对象。
  2. 将非直接缓冲区的内容复制到临时缓冲区。
  3. 使用临时缓冲区执行低级I / O操作。
  4. 临时缓冲区对象超出范围,最终被垃圾回收。

这可能会导致每个I / O上的缓冲区复制和对象流失,这正是我们想要避免的事情。但是,根据实现的不同,情况可能还不错。运行时可能会缓存和重用直接缓冲区,或者执行其他巧妙的技巧来提高吞吐量。如果您只是创建一个供一次性使用的缓冲区,则差异并不明显。另一方面,如果您将在高性能场景中重复使用缓冲区,则最好分配直接缓冲区并重新使用它们。

直接缓冲区是I / O的最佳选择,但创建起来可能会比非直接字节缓冲区更昂贵。直接缓冲区使用的内存是通过绕过标准JVM堆调用本地特定于操作系统的代码来分配的。根据主机操作系统和JVM的实现,设置和拆除直接缓冲区可能比堆驻留缓冲区昂贵得多。直接缓冲区的内存存储区不受垃圾收集的影响,因为它们在标准JVM堆之外。

使用直接缓冲区与非直接缓冲区的性能折衷可能因JVM,操作系统和代码设计而有很大差异。通过在堆外部分配内存,您可能会使您的应用程序受到JVM不了解的其他压力。使其他活动部件发挥作用时,请确保达到预期的效果。我建议使用旧的软件准则:首先使其运行,然后使其快速运行。不必太担心预先优化;首先专注于正确性。JVM实现可能能够执行缓冲区缓存或其他优化,从而为您提供所需的性能,而无需您付出很多不必要的努力。


9
我不喜欢那句话,因为它包含了太多的猜测。此外,当为非直接ByteBuffer进行IO时,JVM当然不需要分配直接ByteBuffer:只需在堆上分配一个字节序列,执行IO,从字节复制到ByteBuffer并释放字节即可。这些区域甚至可以被缓存。但是,完全没有必要为此分配Java对象。真正的答案只能通过测量获得。上次我进行测量时,没有显着差异。我将不得不重做测试以提出所有具体细节。
罗伯特·克莱姆

4
描述NIO(和本机操作)的书是否可以确定性是一个问题。毕竟,不同的JVM和操作系统对事物的管理方式不同,因此不能责怪作者无法保证某些行为。
Martin Tuskevicius

@ RobertKlemme,+ 1,我们都很讨厌猜测,但是,由于存在太多的主要操作系统,因此可能无法衡量所有主要操作系统的性能。另一篇文章 试图这样做,但是我们可以看到它的基准测试有很多问题,首先是“结果根据操作系统而波动很大”。另外,如果有一个败坏的家伙在每个I / O上执行诸如缓冲区复制之类的可怕工作,该怎么办?然后,由于那只羊,我们可能不得不避免编写否则会使用的代码,只是为了避免这些最坏的情况。
Pacerier 2014年

@RobertKlemme我同意。这里有太多的猜测。例如,JVM几乎不可能稀疏地分配字节数组。
洛恩侯爵,2015年

@Edwin Dalorzo:为什么在现实世界中我们需要这样的字节缓冲区?他们是否被发明为在进程之间共享内存的黑客?假设JVM运行在一个进程上,而它又是另一个运行在网络或数据链路层上的进程-负责传输数据-这些字节缓冲区是否分配给这些进程之间共享内存?请纠正我,如果我错了..
汤姆·泰勒

25

没有理由期望直接缓冲区,以更快访问内部的JVM。当您将它们传递给本机代码时(例如,各种渠道背后的代码),它们的优势就会显现出来。


确实。例如,当需要在Scala / Java中进行IO并调用具有大量内存数据的嵌入式Python /本机库进行算法处理时,或将数据直接馈入Tensorflow中的GPU。
SemanticBeeng

21

因为DirectByteBuffers是OS级别的直接内存映射

他们不是。它们只是正常的应用程序进程内存,而在Java GC期间不会进行重定位,从而大大简化了JNI层中的内容。您所描述的适用于MappedByteBuffer

使用get / put调用可以更快地执行

结论并非来自前提。前提是假的;结论也是错误的。他们更快一旦你的JNI层内,如果你正在阅读和同写DirectByteBuffer他们是快,因为数据从来没有越过边界JNI在所有。


7
这是一个很好的和重要的一点:IO的道路上,你必须穿越爪哇- JNI边境的一些点。直接字节缓冲区和非直接字节缓冲区只能移动边界:使用直接缓冲区时,来自Java land的所有放置操作都必须交叉,而使用非直接缓冲区时,所有IO操作都必须交叉。更快的速度取决于应用程序。
罗伯特·克莱姆

@RobertKlemme您的总结不正确。使用所有缓冲区,来回Java的任何数据都必须越过JNI边界。直接缓冲区的要点是,如果您只是将数据从一个通道复制到另一个通道(例如上载文件),则完全不必将其放入Java中,这要快得多。
洛恩侯爵

我的摘要到底在哪里不正确?什么是“摘要”?我在明确地谈论“从Java领域进行放置操作”。如果您仅在Channel之间复制数据(即,不必在Java领域中处理数据),那将是另一回事了。
罗伯特·克莱姆

@RobertKlemme您的陈述“使用直接缓冲区(仅)必须从Java土地进行的所有放置操作都必须交叉”是不正确的。买入和卖出都必须交叉。
洛恩侯爵,

EJP,您显然仍然缺少预期的区别,@ RobertKlemme通过选择在一个短语中使用“ put操作”一词并在句子的对比短语中使用“ IO操作”一词来进行区分。在后一个短语中,他的意图是指缓冲区与某种OS提供的设备之间的操作。
naki

18

最好自己做测量。快速答案似乎是,从allocateDirect()缓冲区发送所需的时间比allocate()变体(测试为将文件复制到/ dev / null)所花费的时间少25%至75%,具体取决于大小,但是分配本身可能会明显变慢(即使100倍)。

资料来源:


谢谢。我会接受您的回答,但我正在寻找有关性能差异的一些更具体的细节。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.