何时使用Array,Buffer或direct Buffer

在编写用于OpenGL库的Matrix类时,我遇到了是否使用Java数组或缓冲区策略来存储数据的问题(JOGL为Matrix操作提供了直接缓冲区复制)。 为了分析这个,我写了一个小的性能测试程序,它比较了Arrays与Buffers vs direct Buffers的循环和批量操作的相对速度。

我想在这里与你分享我的成果(因为我觉得它们很有趣)。 请随时评论和/或指出任何错误。
可以在pastebin.com/is7UaiMV上查看代码。

笔记

  • 循环读取数组实现为A [i] = B [i] ,否则JIT优化器将完全删除该代码。 实际var = A [i]似乎几乎相同。

  • 在数组大小为10,000的示例结果中,JIT优化器很可能已使用类似System.arraycopy的实现替换了循环数组访问。

  • 没有批量获取缓冲区 – >缓冲区,因为Java将A.get(B)实现B.put(A) ,因此结果与批量放置结果相同。

结论

在几乎所有情况下,强烈建议使用Java内部数组。 不仅输出/获取速度大大加快,JIT还能够对最终代码执行更好的优化。

只有在以下两种情况适用时才应使用缓冲区:

  • 您需要处理大量数据。
  • 该数据大部分或总是经过批量处理

请注意,后备缓冲区具有Java数组,用于补偿缓冲区的内容。 建议在此缓冲区上执行操作,而不是循环put / get。

只有在担心内存使用情况且永远不会访问基础数据时, 应使用直接缓冲区。 它们比非直接缓冲区稍慢,如果访问基础数据则要慢得多,但使用的内存较少。 此外,在使用直接缓冲区时,将非字节数据(如float-arrays)转换为字节时会产生额外的开销。

有关详细信息,请参阅此处

  • 为什么只有ByteBuffers在使用直接缓冲区时才有用
  • NIO的内部开销以及减缓缓冲区的速度

样本结果

注意:百分比仅为便于阅读而没有实际意义。

使用大小为16的数组和10,000,000次迭代……

-- Array tests: ----------------------------------------- Loop-write array: 87.29 ms 11,52% Arrays.fill: 64.51 ms 8,51% Loop-read array: 42.11 ms 5,56% System.arraycopy: 47.25 ms 6,23% -- Buffer tests: ---------------------------------------- Loop-put buffer: 603.71 ms 79,65% Index-put buffer: 536.05 ms 70,72% Bulk-put array->buffer: 105.43 ms 13,91% Bulk-put buffer->buffer: 99.09 ms 13,07% Bulk-put bufferD->buffer: 80.38 ms 10,60% Loop-get buffer: 505.77 ms 66,73% Index-get buffer: 562.84 ms 74,26% Bulk-get buffer->array: 137.86 ms 18,19% -- Direct buffer tests: --------------------------------- Loop-put bufferD: 570.69 ms 75,29% Index-put bufferD: 562.76 ms 74,25% Bulk-put array->bufferD: 712.16 ms 93,96% Bulk-put buffer->bufferD: 83.53 ms 11,02% Bulk-put bufferD->bufferD: 118.00 ms 15,57% Loop-get bufferD: 528.62 ms 69,74% Index-get bufferD: 560.36 ms 73,93% Bulk-get bufferD->array: 757.95 ms 100,00% 

使用大小为1,000且100,000次迭代的数组…

 -- Array tests: ----------------------------------------- Loop-write array: 22.10 ms 6,21% Arrays.fill: 10.37 ms 2,91% Loop-read array: 81.12 ms 22,79% System.arraycopy: 10.59 ms 2,97% -- Buffer tests: ---------------------------------------- Loop-put buffer: 355.98 ms 100,00% Index-put buffer: 353.80 ms 99,39% Bulk-put array->buffer: 16.33 ms 4,59% Bulk-put buffer->buffer: 5.40 ms 1,52% Bulk-put bufferD->buffer: 4.95 ms 1,39% Loop-get buffer: 299.95 ms 84,26% Index-get buffer: 343.05 ms 96,37% Bulk-get buffer->array: 15.94 ms 4,48% -- Direct buffer tests: --------------------------------- Loop-put bufferD: 355.11 ms 99,75% Index-put bufferD: 348.63 ms 97,93% Bulk-put array->bufferD: 190.86 ms 53,61% Bulk-put buffer->bufferD: 5.60 ms 1,57% Bulk-put bufferD->bufferD: 7.73 ms 2,17% Loop-get bufferD: 344.10 ms 96,66% Index-get bufferD: 333.03 ms 93,55% Bulk-get bufferD->array: 190.12 ms 53,41% 

使用大小为10,000且100,000次迭代的数组…

 -- Array tests: ----------------------------------------- Loop-write array: 156.02 ms 4,37% Arrays.fill: 109.06 ms 3,06% Loop-read array: 300.45 ms 8,42% System.arraycopy: 147.36 ms 4,13% -- Buffer tests: ---------------------------------------- Loop-put buffer: 3385.94 ms 94,89% Index-put buffer: 3568.43 ms 100,00% Bulk-put array->buffer: 159.40 ms 4,47% Bulk-put buffer->buffer: 5.31 ms 0,15% Bulk-put bufferD->buffer: 6.61 ms 0,19% Loop-get buffer: 2907.21 ms 81,47% Index-get buffer: 3413.56 ms 95,66% Bulk-get buffer->array: 177.31 ms 4,97% -- Direct buffer tests: --------------------------------- Loop-put bufferD: 3319.25 ms 93,02% Index-put bufferD: 3538.16 ms 99,15% Bulk-put array->bufferD: 1849.45 ms 51,83% Bulk-put buffer->bufferD: 5.60 ms 0,16% Bulk-put bufferD->bufferD: 7.63 ms 0,21% Loop-get bufferD: 3227.26 ms 90,44% Index-get bufferD: 3413.94 ms 95,67% Bulk-get bufferD->array: 1848.24 ms 51,79% 

直接缓冲区并不意味着加速Java代码的访问。 (如果可能的话,JVM自己的数组实现有问题。)

这些字节缓冲区用于与其他组件连接,因为您可以将字节缓冲区写入ByteChannel并且可以将直接缓冲区与本机代码结合使用,例如使用您提到的OpenGL库。 它旨在加速这些操作。 使用图形卡的芯片进行渲染可以加速整体操作,而不仅仅是补偿从Java代码中缓慢访问缓冲区的程度。

顺便说一句,如果你测量字节缓冲区的访问速度,特别是直接字节缓冲区,在获取FloatBuffer视图之前,值得将字节顺序更改为本字节顺序:

 FloatBuffer bufferD = ByteBuffer.allocateDirect(SIZE * 4) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); 

Tldr:

在我们需要执行高效的高速I / O 时才使用直接缓冲区。

如果我们需要高效的高速非I / O操作,默认数组是最佳选择。

如果我们需要在默认数组上执行类似缓冲区的操作, 并且我们可以放慢速度 ,那么使用数组支持的缓冲区。

TSDR:

您的测试没有测试任何I / O操作,因此结论是错误的。

你的结论陈述(强调不是我的):

只有在担心内存使用情况且永远不会访问基础数据时, 应使用直接缓冲区。 它们比非直接缓冲区稍慢,如果访问基础数据则要慢得多,但使用的内存较少。 此外,在使用直接缓冲区时,将非字节数据(如float-arrays)转换为字节时会产生额外的开销。

这显然是错误的。 直接缓冲区旨在解决速度问题 ,而不是内存问题。 无论何时需要高性能I / O访问,都应使用直接缓冲区。 这包括文件/网络操作等。 如果使用得当 ,它肯定更快 ,实际上是Java API提供的最快的开箱即用。

在进行文件/网络操作时,将非字节数据转换为字节时会产生额外的开销。 这对所有事情都是如此,而不仅仅是直接缓冲。

你的结论还指出:

请注意,后备缓冲区具有Java数组,用于补偿缓冲区的内容。 建议在此缓冲区上执行操作,而不是循环put / get。

这是事实,但您缺少数组支持的缓冲区的全部内容。 数组支持的缓冲区是数组顶部的外观模式 。 支持数组的缓冲区永远不会比数组本身更快,因为内部它们必须使用数组。

因此,它们是为了方便而不是速度。 换句话说,如果你需要速度,建议选择array over array-facade。 如果您需要方便/可读性,建议在arrays上选择array-facade over array进行缓冲区操作。

另请阅读:

  • 字节缓冲区如何工作以及为什么只有直接缓冲区才有用 (重要!)

  • Java NIO的摘录