何时使用Array、Buffer或直接Buffer

29

问题

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

我想在这里分享我的结果(因为我觉得它们非常有趣)。请随意评论和/或指出任何错误。
代码可在pastebin.com/is7UaiMV中查看。

注释

  • 循环读取数组实现为A [i] = B [i],否则JIT优化器将完全删除该代码。实际的var = A [i]看起来差不多。

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

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

结论

在几乎所有情况下,强烈建议使用Java内部数组。不仅是put/get速度要快得多,JIT在最终代码上的优化也更好。

只有同时满足以下两个条件才应该使用缓冲区:

  • 您需要处理大量数据。
  • 这些数据大多数或总是进行批量处理

请注意,后端缓冲区具有支持缓冲区内容的Java Array。建议对此后缓冲区执行操作,而不是循环放置/获取。

只有当您担心内存使用并且从不访问底层数据时,才应使用直接缓冲区。它们比非直接缓冲区稍慢,如果访问底层数据,则慢得多,但使用的内存较少。此外,在使用直接缓冲区将非字节数据(如浮点数组)转换为字节时,会有额外的开销。

更多细节请参见此处:

示例结果

注意:百分比仅供阅读方便,没有实际意义。

使用大小为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%

3
这些数字让 API 看起来很糟糕。你把它设置为直接类型缓冲器来进行测试了吗?这样可以实现更好的 JIT 优化,快捷地执行所有后备数组等。也会有其他成本,但可能会有所帮助。 - ingyhere
@TwoThe,请将您的结论作为单独的答案发布,以便人们可以分别对您的问题和答案进行评论和投票。 - Pacerier
太棒了!干得好。 - user2517419
2个回答

11

直接缓冲区不是用来加速Java代码访问的。(如果这是可能的,那么JVM自己的数组实现就有问题了。)

这些字节缓冲区是为了与其他组件交互而存在的,因为您可以将字节缓冲区写入ByteChannel中,并且您可以与本机代码一起使用直接缓冲区,例如OpenGL库。它旨在加速这些操作。使用图形卡芯片进行渲染可以加速整个操作,弥补了从Java代码访问缓冲区可能较慢的情况。

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

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

5

简述:

仅当我们需要进行高效的高速I/O时,才使用直接缓冲区。

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

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

简化版:

只有在需要进行高效的高速I/O时才使用直接缓冲区。如果需要进行高效的高速非I/O操作,则使用默认数组。如果需要在默认数组上执行缓冲区操作,并且可以承受较慢的速度,则使用支持数组的缓冲区。

反驳:

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

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

仅当您担心内存使用情况并且从不访问基础数据时,才应使用直接缓冲区。它们比非直接缓冲区稍慢,如果访问底层数据,则慢得多,但使用的内存较少。此外,在使用直接缓冲区时,将非字节数据(例如浮点数组)转换为字节时会有额外的开销。

显然是错误的。直接缓冲区旨在解决速度问题,而不是内存问题。只有在需要高性能I/O访问时才应使用直接缓冲区。这包括文件/网络操作等。如果正确使用,它绝对比其他方法更快,并且实际上是Java API提供的最快速度。

进行文件/网络操作时,将非字节数据转换为字节时会有额外的开销。这对于所有内容都是真实的,而不仅仅是直接缓冲区。

你的结论还声明:

请注意,后端缓冲区具有Java数组支持缓冲区内容。建议在此后端缓冲区上执行操作,而不是循环放置/获取。

这是正确的,但您错过了支持数组的缓冲区的全部意义。支持数组的缓冲区是基于数组的facade pattern。由于内部必须使用数组,因此支持数组的缓冲区永远不会比数组本身更快。

因此,它们是为方便而存在,而不是为了速度。换句话说,如果需要速度,则建议选择数组而不是支持数组的缓冲区。如果需要方便/可读性,则建议在数组上执行类似缓冲区的操作时选择支持数组的缓冲区而不是数组。

另请阅读:


你在这里误解和曲解了一些事情,而且你的一些评论甚至与引语无关。如果你进一步遵循你对直接IO的想法,你会得出我上面写的相同结论。第二个引语也是如此。 - TwoThe
1
@TwoThe,1) 您说过“仅在担心内存使用并且从未访问底层数据时才应使用直接缓冲区”。这是错误的,因为如果需要效率,则应该使用直接缓冲区进行I/O。2) 您说过“它们[直接缓冲区]比非直接缓冲区稍慢,如果访问底层数据则慢得多,但使用的内存较少”。这是错误的,因为无论我们是否需要从JVM访问I/O,直接缓冲区对于I/O来说都比非直接缓冲区更快。 - Pacerier
  1. 你说过:“使用直接缓冲区时,将非字节数据(如浮点数数组)转换为字节时会增加额外的开销。” 假设我们正在进行I/O操作,这不是直接缓冲区的一个缺点,因为默认数组和非直接缓冲区在进行I/O时也需要将非字节数据转换为字节。
  2. 你的测试没有测试单个文件的读写。为什么?
- Pacerier
我不会与您讨论信仰问题。如果您认为我的结论是错误的,请编写适当的测试代码并提供事实证明。 - TwoThe
2
@TwoThe,我的帖子已经提出了事实。请查看我提供的链接中的链接:例如,在http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4879883的错误数据库中的线程。此外,你为什么说《Java NIO》这本书不是事实呢? - Pacerier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接