【问题标题】:Java Multi-Dimensional Array: Memory Address and Traversal SpeedJava多维数组:内存地址和遍历速度
【发布时间】:2015-04-10 15:34:04
【问题描述】:

我了解在 Java 中处理二维数组时,您在循环中访问数组元素的顺序会影响遍历数组所需的时间:

int size = 500;
int[][] array = new int[size][size];

// Slower
for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
        array[j][i] = 1;
    }
}

// Faster
for (int i = 0; i < size; i++) {
    for (int j = 0; j < size; j++) {
        array[i][j] = 1;
    }
}

这对我来说很有意义,因为它需要更少的内存跳转,而是可以直接跳转到后续地址。

当对 3 维数组做同样的事情时,我对结果有点困惑:

Time:  12356332 nanoseconds. ([i][j][k])
Time:  18278948 nanoseconds. ([i][k][j])
Time:  13985288 nanoseconds. ([j][i][k])
Time: 126192723 nanoseconds. ([j][k][i])
Time:  39441820 nanoseconds. ([k][i][j])
Time: 156352618 nanoseconds. ([k][j][i])

[i][j][k][j][i][k] 的结果在大多数代码运行中是可互换的。这是为什么呢?

另外,您能否解释一下多维数组在 Java 中是如何存储的?

给定数组int[][][] array = new int[2][2][2],内存地址是否会像这样(我的理解是每个块之间可能还有其他变量的附加数据,但我省略了这些情况,因为它们不相关 em>): (对不起,如果图像令人困惑,我只有油漆可以使用,并试图尽可能地表达布局。所以array[0][0][0] //[i][j][k] 将在地址06 中)

【问题讨论】:

  • 你是如何测量这个的?在 Java 中准确测量这些东西的性能(甚至对于“A 比 B 更好”来说足够准确)真的非常困难
  • 我在每个循环前后都使用了System.nanoTime()并减去了差异
  • 是的,不,这不会为您提供准确的结果。如果您想获得可以实际告诉您 A 更快/更慢/等于 B 的测量值,请使用 JMH 或 Caliper。
  • Java 实现数组的技术术语是锯齿状数组。见en.m.wikipedia.org/wiki/Jagged_array

标签: java arrays memory


【解决方案1】:

首先要考虑的是,仔细观察,java 没有多维数组,因为它是一个单一的实体。相反,java 只处理一维数组,但元素类型本身可以是数组类型,并且语言/编译器支持与例如相同的语法。 C 为多个维度提供快捷方式寻址一个元素。

例如int[][] twoDim = new int[50][100];实际上在内存中创建了51个对象; 一个 int[][] 类型的数组,其中有 50 个 int[] 类型的元素空间,然后用 int[100] 类型的数组填充这 50 个空间(剩下的 50 个对象)。这 51 个对象中的每一个都是独立的,它们可以位于堆中的任意位置。实际上它们甚至不需要在同一个语句中创建。

以下两种方法给出相同的数组作为结果,但第二种方法应该清楚真正发生了什么幕后

 public int[][] createArrayA(int n, int m) {
     return new int[n][m];
 }

 public int[][] createArrayB(int n, int m) {
     int[][] array = new int[n][];
     for (int i=0; i<n; ++i)
         array[i] = new int[m];
     return array;
 }

请注意,在 createArrayB() 中,您也可以选择向后初始化 n 维(循环倒数而不是倒数),从而得到相同的数组:

 public int[][] createArrayC(int n, int m) {
     int[][] array = new int[n][];
     for (int i=n-1; i>=0; --i)
         array[i] = new int[m];
     return array;
 }

变体 B 和 C 的内存布局会不同,因为它们的分配顺序不同。但是不要假设它们的内存布局是一个常数,垃圾收集器稍后可能会将它们移动到堆中。

如果你关心访问速度,迭代数组的最快方法总是最左维度进入最外层循环最右边的维度进入最内层循环(这是严格围绕各个维度在内存中线性定位的事实而构建的)。并且 CPU 使用线性内存访问比使用随机访问更快(我不会在这里讨论为什么)。

在处理数组时可以考虑两个微优化

首先是尺寸的顺序,当您可以随意订购尺寸时,将最小的放在最左边,最大的放在最右边

int[][] slowArray = new int[10000][2];
int[][] fastArray = new int[2][10000];

第二个也节省了大量内存,因为慢速变体由 10000 x int[2] = 10001 个对象组成,而快速变体由 2 x int[10000] = 3 个对象组成。

第二个是使用数组维度的切片(它是代码不变移动的一种形式):

long sum = 0;
int[][] fastArray = new int[2][10000];
for (int i=0; i<fastArray.length; ++i) {
    int[] subArray = fastArray[i];
    for (int j=0; j<subArray.length; ++j) {
        sum += subArray[j];
    }
}

定义一个局部变量 subArray 完全消除了内部循环的外部维度(毕竟,内部循环中 i 从不改变,所以为什么每次要解析 j 时都查找数组索引 i 呢?)。这种优化可能由即时编译器自动执行,但据我所知,它并非总是自动执行。偶尔的循环无关紧要,但如果数组遍历占用了您处理时间的很大一部分,则需要考虑进行优化。

【讨论】:

  • 感谢您的解释。我不太理解存储数组数组背后的想法......就字节码而言,没有什么能阻止使用单个连续内存块以经典方式定义 n 维数组以获得更可预测的性能。
  • @edTarik 阻止您将多维数组定义为连续内存块的是java语言规范本身,特别是每个数组是(也是)一个对象我>。通过 createArrayB() 示例逐步(实际上或更好地在调试器中)并查看整个方法中的数组内容。此外,您可能需要检查术语“锯齿状数组”(此功能也是数组数组组织的结果)。
  • 嗯,您可以通过使用索引轻松获得使用一维数组的二维数组的等价物。这就是 C 在内部的方式。因此,JVM 中没有任何东西可以阻止向 Java 编程语言添加语法糖来处理 2D 数组以及更传统的锯齿状数组。
  • @edTarik 我明白你的意思,但这仍然需要在语言级别进行更改;不仅仅是虚拟机级别。仅使用(纯)语法糖所能达到的效果是有限度的(看看我的最后一个代码示例,其中包含 why 的代码不变移动,这将是一个非常基本的补充)。您仍然可以通过自己(使用当前的 java 语言元素)进行索引数学来模拟经典的 C 多维数组,同时如果您觉得它足以让您值得付出努力,则实际将数据存储在单维数组中。
  • 我看了一下代码不变代码。我仍然不太了解使用添加 Java 语法结构来做到这一点的原因:使用一维数组并使用未更改的 JVM 进行索引数学运算。
猜你喜欢
  • 1970-01-01
  • 2012-04-16
  • 2013-03-14
  • 2015-09-26
  • 1970-01-01
相关资源
最近更新 更多