Java多维数组:内存地址和遍历速度

Java Multi-Dimensional Array: Memory Address and Traversal Speed

我了解到在 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] 内存地址会像这样吗(我的理解是每个块之间的其他变量可能有额外的数据,但我省略了这些情况,因为它们不相关): (抱歉,如果图像令人困惑,我只能使用绘画并尽力表达布局。所以 array[0][0][0] //[i][j][k] 将在地址 06 中)

首先要考虑的是,java 仔细观察,没有多维数组,因为它是一个单一的实体。相反,java 仅处理单维数组,但元素类型本身可以是数组类型,并且 language/compiler 支持与例如相同的语法。 C 对多个维度进行快捷寻址元素。

例如int[][] twoDim = new int[50][100];实际上在内存中创建了51个对象; 一个 类型 int[][] 的数组,其中 space 用于 50 个 int[] 类型的元素,并用数组填充这 50 个 space int[100] 类型(生成剩余的 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?)。这种优化 可能 由即时编译器自动执行,但据我所知,它不会 总是 自动执行。这对于偶尔的循环并不重要,但如果数组遍历占据了你处理时间的很大一部分,那么它就是一个需要考虑的优化。