【问题标题】:Java: Multi-dimensional array vs. one-dimensionalJava:多维数组与一维
【发布时间】:2011-01-31 12:13:46
【问题描述】:

例如:

  • a) int [x][y][z]

  • b) int[x*y*z]

为了简单起见,我最初认为我会选择 a)。

我知道 Java 不像 C 那样将数组线性存储在内存中,但这对我的程序有什么影响?

【问题讨论】:

标签: java arrays multidimensional-array


【解决方案1】:

在搜索此类问题的答案时,通常最好的办法是查看这些选项是如何编译成 JVM 字节码的:

multi = new int[50][50];
single = new int[2500];

这被翻译成:

BIPUSH 50
BIPUSH 50
MULTIANEWARRAY int[][] 2
ASTORE 1
SIPUSH 2500
NEWARRAY T_INT
ASTORE 2

所以,如您所见, JVM 已经知道我们说的是多维数组。

继续下去:

for (int i = 0; i < 50; ++i)
    for (int j = 0; j < 50; ++j)
    {
        multi[i][j] = 20;
        single[i*50+j] = 20;
    }

这被翻译(跳过循环)成:

ALOAD 1: multi
ILOAD 3: i
AALOAD
ILOAD 4: j
BIPUSH 20
IASTORE

ALOAD 2: single
ILOAD 3: i
BIPUSH 50
IMUL
ILOAD 4: j
IADD
BIPUSH 20
IASTORE

所以, 如你看到的, 多维数组在 VM 内部处理, 没有无用指令产生的开销, 而使用单个则使用更多指令,因为偏移量是手动计算的。

我认为性能不会成为这样的问题。

编辑:

我做了一些简单的基准测试来看看这里发生了什么。 我选择尝试不同的例子: 线性读取, 线性写入, 和随机访问。 时间以毫秒为单位(使用System.nanoTime() 计算。 结果如下:

线性写入

  • 尺寸:100x100 (10000)
    • 多路:5.786591
    • 单身:6.131748
  • 尺寸:200x200 (40000)
    • 多路:1.216366
    • 单:0.782041
  • 尺寸:500x500 (250000)
    • 多路:7.177029
    • 单身:3.667017
  • 尺寸:1000x1000 (1000000)
    • 多路:30.508131
    • 单身:18.064592
  • 尺寸:2000x2000 (4000000)
    • 多:185.3548
    • 单身:155.590313
  • 尺寸:5000x5000 (25000000)
    • 多:955.5299
    • 单身:923.264417
  • 尺寸:10000x10000 (100000000)
    • 多:4084.798753
    • 单身:4015.448829

线性读取

  • 尺寸:100x100 (10000)
    • 多路:5.241338
    • 单身:5.135957
  • 尺寸:200x200 (40000)
    • 多:0.080209
    • 单:0.044371
  • 尺寸:500x500 (250000)
    • 多:0.088742
    • 单:0.084476
  • 尺寸:1000x1000 (1000000)
    • 多:0.232095
    • 单:0.167671
  • 尺寸:2000x2000 (4000000)
    • 多:0.481683
    • 单:0.33321
  • 尺寸:5000x5000 (25000000)
    • 多:1.222339
    • 单:0.828118
  • 尺寸:10000x10000 (100000000)
    • 多路:2.496302
    • 单:1.650691

随机读取

  • 尺寸:100x100 (10000)
    • 多:22.317393
    • 单身:8.546134
  • 尺寸:200x200 (40000)
    • 多:32.287669
    • 单身:11.022383
  • 尺寸:500x500 (250000)
    • 多:189.542751
    • 单身:68.181343
  • 尺寸:1000x1000 (1000000)
    • 多:1124.78609
    • 单身:272.235584
  • 尺寸:2000x2000 (4000000)
    • 多:6814.477101
    • 单身:1091.998395
  • 尺寸:5000x5000 (25000000)
    • 多:50051.306239
    • 单身:7028.422262

随机数有点误导,因为它为多维数组生成 2 个随机数,而为一维数组生成一个随机数(PNRG 可能会消耗一些 CPU)。

请注意,我试图让 JIT 仅在同一循环的第 20 次运行后进行基准测试。为了完整起见,我的 java VM 如下:

java 版本“1.6.0_17” Java(TM) SE 运行时环境 (build 1.6.0_17-b04) Java HotSpot(TM) 64 位服务器 VM(内部版本 14.3-b01,混合模式)

【讨论】:

  • 总是很高兴看到有人看到幕后的现实,而不是仅仅做出假设。如果可以的话,我会给你 +100。
  • 到代码被jitted的时候,JVM指令的数量已经无关紧要了。重要的是代码运行所需的实际时间,这取决于位置、取消引用和内存使用等因素。
  • 请更新随机读取基准,使其为两个版本生成 2 个随机数。可能单数组版本甚至会更快,因为需要更少的内存查找(随机读取会产生最多的缓存未命中),但在测量之前你永远无法确定。
  • 在您的消息的第一部分中,您得出结论认为字节码是相似的并且不会有性能差异,但是您消息后半部分的基准证明您最初的假设是错误的。这强化了“过早的优化是万恶之源”的观点,因为试图猜测性能很少奏效。 :) 我在答案中添加了 3 维数组的基准,并且还考虑了生成随机数的开销。
  • 实际上,从您展示的字节码中可以看出,多维数组可能会更慢:它需要 2 次堆访问(AALOAD 和 IASTORE),而单维版本只需要 1 次堆访问(IASTORE) .所有其他指令都对堆栈上的值(将在缓存或寄存器中)进行操作或进行算术运算,因此它们非常快。
【解决方案2】:

在当前的 CPU 上,非缓存内存访问比算术慢数百倍(请参阅 this presentation 并阅读 What every programmer should know about memory)。 a) 选项将导致大约 3 次内存查找,而 b) 选项将导致大约 1 次内存查找。 CPU 的预取算法也可能无法正常工作。所以 b) 选项在某些情况下可能更快(这是一个热点并且阵列不适合 CPU 的缓存)。快多少? - 这将取决于应用程序。

我个人会首先使用 a) 选项,因为它会产生更简单的代码。如果分析器显示数组访问是一个瓶颈,那么我会将其转换为 b) 选项,以便有一对用于读取和写入数组值的辅助方法(这样混乱的代码将仅限于这两个方法)。

我为比较 3 维 int 数组(“Multi”列)和等效的 1 维 int 数组(“Single”列)做了一个基准测试。代码是here,测试here。我使用 JVM 选项 -server -Xmx3G -verbose:gc -XX:+PrintCompilation 在 64 位 jdk1.6.0_18、Windows 7 x64、Core 2 Quad Q6600 @ 3.0 GHz、4 GB DDR2 上运行它(我已从以下结果中删除了调试输出)。结果是:

Out of 20 repeats, the minimum time in milliseconds is reported.

Array dimensions: 100x100x100 (1000000)
            Multi   Single
Seq Write   1       1
Seq Read    1       1
Random Read 99      90    (of which generating random numbers 59 ms)

Array dimensions: 200x200x200 (8000000)
            Multi   Single
Seq Write   14      13
Seq Read    11      8
Random Read 1482    1239    (of which generating random numbers 474 ms)

Array dimensions: 300x300x300 (27000000)
            Multi   Single
Seq Write   53      46
Seq Read    34      24
Random Read 5915    4418    (of which generating random numbers 1557 ms)

Array dimensions: 400x400x400 (64000000)
            Multi   Single
Seq Write   123     111
Seq Read    71      55
Random Read 16326   11144    (of which generating random numbers 3693 ms)

这表明一维数组更快。尽管差异如此之小,但对于 99% 的应用程序而言,差异并不明显。

我还进行了一些测量,以估计在随机读取基准中生成随机数的开销,方法是将preventOptimizingAway += array.get(x, y, z); 替换为preventOptimizingAway += x * y * z;,并将测量结果手动添加到上述结果表中。生成随机数只需要 Random Read 基准测试总时间的 1/3 或更少,因此内存访问按预期支配了基准测试。用 4 维或更多维的数组重复这个基准测试会很有趣。可能它会使速度差异更大,因为多维数组的最顶层将适合 CPU 的缓存,而只有其他级别需要内存查找。

【讨论】:

    【解决方案3】:

    使用第一个变体(3 维),因为它更容易理解,并且发生逻辑错误的机会更少(尤其是当您使用它来建模 3 维空间时)

    【讨论】:

      【解决方案4】:

      如果您选择后一种路线,那么您将不得不为每个数组访问执行算术运算。这会很痛苦并且容易出错(除非您将它包装在提供此功能的类中)。

      我认为在选择平面数组时没有任何(显着)优化(特别是考虑到索引它所采用的算法)。与优化一样,您需要执行一些测量并确定它是否真的值得。

      【讨论】:

      • 好的,谢谢。我只是打算使用一个 3 维数组,如果我遇到性能问题,请进行比较。
      • 如果您使用多维数组,那么您将不得不为每个单独的数组访问执行多次内存访问,这可能比一点算术慢buch。但是,是的,对于这种事情,您确实需要在采取行动之前进行衡量。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-10-17
      • 1970-01-01
      • 1970-01-01
      • 2014-01-13
      • 1970-01-01
      相关资源
      最近更新 更多