【问题标题】:When to use Array, Buffer or direct Buffer何时使用 Array、Buffer 或直接 Buffer
【发布时间】:2013-09-20 09:15:00
【问题描述】:

问题

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

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

注意事项

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

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

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

结论

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

缓冲区应该两者都适用的情况下使用:

  • 您需要处理大量数据。
  • 这些数据大部分或总是批量处理

请注意,backened-buffer 有一个 Java Array 来支持缓冲区的内容。建议对这个后备缓冲区进行操作,而不是循环 put/get。

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

更多详情请看这里:

示例结果

注意:百分比只是为了便于阅读,没有实际意义。

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

【问题讨论】:

  • 这些数字让 API 看起来很糟糕。您是否将其设置为 Direct 类型的缓冲区来进行测试?这可以实现更好的 JIT 优化、所有支持数组的快捷方式等。其他成本,但它可能会有所帮助。
  • @TwoThe,请将您的结论作为单独的答案发布,以便人们可以对您的问题和您的答案进行不同的评论和投票。
  • 太棒了!。伟大的工作。

标签: java arrays performance buffer


【解决方案1】:

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

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

顺便说一句,如果您测量对字节缓冲区(尤其是直接字节缓冲区)的访问速度,则值得在获取FloatBuffer 视图之前将字节顺序更改为本机字节顺序:

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

【讨论】:

    【解决方案2】:

    Tldr:

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

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

    如果我们需要对默认数组执行类似缓冲区的操作,并且我们可以承受,然后使用数组支持的缓冲区。

    Tsdr:

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

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

    如果您担心内存,则应使用直接缓冲区 使用,从不访问基础数据。他们稍微慢一点 比非直接缓冲区慢得多,如果基础数据是 访问,但使用较少的内存。此外还有额外的开销 将非字节数据(如浮点数组)转换为字节时 使用直接缓冲区。

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

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

    你的结论还指出:

    请注意,backened-buffer 有一个 Java Array 支持 缓冲区。建议对这个后备缓冲区进行操作 而不是循环 put/get。

    这是真的,但是你错过了数组支持的缓冲区的全部意义。 数组支持的缓冲区是数组顶部的facade pattern。数组支持的缓冲区永远不会比数组本身快,因为它们在内部必须使用数组。

    因此,它们的存在是为了方便,而不是为了速度。换句话说,如果您需要速度,建议选择 array 而不是 array-facade。如果您需要方便/可读性,建议在数组上选择数组外观而不是数组来进行类似缓冲区的操作。

    另请阅读:

    【讨论】:

    • 你在这里误解和误解了一些东西,你的一些cmets甚至与报价无关。如果您进一步了解直接 IO 的想法,您将得出与我在上面写的相同的结论。与第二个报价相同。
    • @TwoThe, 1) 您说“只有在担心内存使用并且从不访问底层数据时才应使用直接缓冲区”。这是错误的,因为如果您需要效率,应该将直接缓冲区用于 I/O。 2) 您说“它们 [直接缓冲区] 比非直接缓冲区稍慢,如果访问底层数据则要慢得多,但使用的内存更少”。这是错误的,因为无论我们是否需要从 JVM 访问 I/O,对于 I/O,直接缓冲区都比非直接缓冲区快......
    • ................ 3) 您说“此外,在转换非字节数据(如 float-使用直接缓冲区时将数组)转换为字节”。假设我们在做 I/O 操作,这不是直接缓冲区的缺点,因为默认数组和非直接缓冲区在内部也需要在做 I/O 时将非字节数据转换为字节。 4) 您的测试没有测试单个文件的读/写。为什么?
    • 我不会和你讨论你的信仰。如果您认为我的结论是错误的,请编写适当的测试代码并将事实摆在桌面上。
    • @TwoThe,我的帖子已经将事实摆在桌面上。请参阅我提供的链接中的链接:例如错误数据库中的线程 bugs.java.com/bugdatabase/view_bug.do?bug_id=4879883 。还有,为什么说Java NIO这本书不是事实?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-08
    • 2017-12-26
    • 1970-01-01
    相关资源
    最近更新 更多