C 在二维数组上迭代时,为什么循环的顺序会影响性能?

C 在二维数组上迭代时,为什么循环的顺序会影响性能?,c,performance,for-loop,optimization,cpu-cache,C,Performance,For Loop,Optimization,Cpu Cache,下面是两个几乎相同的程序,只是我切换了I和j变量。它们都在不同的时间内运行。有人能解释一下为什么会这样吗 版本1 #include <stdio.h> #include <stdlib.h> main () { int i,j; static int x[4000][4000]; for (i = 0; i < 4000; i++) { for (j = 0; j < 4000; j++) { x[j][i] = i + j;

下面是两个几乎相同的程序,只是我切换了
I
j
变量。它们都在不同的时间内运行。有人能解释一下为什么会这样吗

版本1

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (i = 0; i < 4000; i++) {
    for (j = 0; j < 4000; j++) {
      x[j][i] = i + j; }
  }
}
#包括
#包括
主要(){
int i,j;
静态整数x[4000][4000];
对于(i=0;i<4000;i++){
对于(j=0;j<4000;j++){
x[j][i]=i+j;}
}
}
版本2

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (j = 0; j < 4000; j++) {
     for (i = 0; i < 4000; i++) {
       x[j][i] = i + j; }
   }
}
#包括
#包括
主要(){
int i,j;
静态整数x[4000][4000];
对于(j=0;j<4000;j++){
对于(i=0;i<4000;i++){
x[j][i]=i+j;}
}
}

与组装无关。这是由于

C多维数组以最后一个维度作为最快的维度存储。因此,第一个版本在每次迭代时都会丢失缓存,而第二个版本不会。因此,第二个版本应该更快


另请参见:。

版本2将运行得更快,因为它比版本1更好地使用计算机的缓存。仔细想想,数组只是内存中连续的区域。当您请求数组中的元素时,您的操作系统可能会将内存页带入包含该元素的缓存中。但是,由于接下来的几个元素也在该页上(因为它们是连续的),下一次访问将已经在缓存中!这就是版本2为提高速度所做的


另一方面,版本1正在按列而不是按行访问元素。这种访问在内存级别是不连续的,因此程序不能充分利用操作系统缓存。

原因是缓存本地数据访问。在第二个程序中,您将线性扫描内存,这得益于缓存和预取。您的第一个程序的内存使用模式更分散,因此缓存行为更差。

这一行是罪魁祸首:

x[j][i]=i+j;
第二个版本使用连续内存,因此速度将大大加快

我试过了

x[50000][50000];

版本1的执行时间是13秒,而版本2的执行时间是0.6秒。

正如其他人所说,问题是数组中内存位置的存储:
x[i][j]
。以下是一些原因:

您有一个二维数组,但计算机中的内存本质上是一维的。所以,当你想象你的阵列是这样的:

0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3
您的计算机将其作为一行存储在内存中:

0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3
在第二个示例中,首先通过循环第二个数字来访问阵列,即:

x[0][0] 
        x[0][1]
                x[0][2]
                        x[0][3]
                                x[1][0] etc...
这意味着你要按顺序打它们。现在看第1个版本。你正在做:

x[0][0]
                                x[1][0]
                                                                x[2][0]
        x[0][1]
                                        x[1][1] etc...
由于C在内存中布局二维数组的方式,您要求它到处跳跃。但现在对于踢球者来说:为什么这很重要?所有的内存访问都是一样的,对吗

否:因为缓存。内存中的数据以小块(称为“缓存线”)的形式(通常为64字节)传送到CPU。如果你有4字节的整数,这意味着你在一个整洁的小束中获得了16个连续的整数。获取这些内存块实际上相当慢;CPU可以在加载单个缓存线所需的时间内完成大量工作

现在回顾访问顺序:第二个示例是(1)获取16个整数的块,(2)修改所有整数,(3)重复4000*4000/16次。这很好,速度也很快,而且CPU总是有工作要做

第一个例子是(1)获取16个整数的块,(2)只修改其中一个,(3)重复4000*4000次。这将需要从内存中“获取”16倍的次数。实际上,你的CPU将不得不花时间坐在那里等待内存的出现,而当它坐在那里的时候,你正在浪费宝贵的时间

重要提示:

现在你已经有了答案,这里有一个有趣的提示:没有内在的原因让你的第二个例子必须是快速的。例如,在Fortran中,第一个示例是快速的,第二个示例是慢速的。这是因为Fortran没有像C那样将内容扩展为概念上的“行”,而是扩展为“列”,即:


C的布局称为“行主”,Fortran的布局称为“列主”。正如您所见,了解您的编程语言是行专业还是列专业非常重要!这里有一个链接提供更多信息:

除了关于缓存命中的其他优秀答案外,还有一个可能的优化差异。编译器可能会将第二个循环优化为与以下内容等效的内容:

  for (j=0; j<4000; j++) {
    int *p = x[j];
    for (i=0; i<4000; i++) {
      *p++ = i+j;
    }
  }
(j=0;j我试图给出一个一般性的答案

因为
i[y][x]
是C语言中
*(i+y*数组宽度+x)
的简写形式(试试classy
int p[3];0[p]=0xBEEF;

当您在
y
上迭代时,您将在
array\u width*sizeof(array\u element)
大小的块上迭代。如果您的内部循环中有这样的块,那么您将在这些块上进行
array\u width*array\u height
迭代

通过翻转顺序,您将只有
array\u height
chunk迭代,在任何chunk迭代之间,您将只有
array\u width
迭代
sizeof(array\u element)


虽然在非常旧的x86 CPU上,这并不重要,但现在的x86做了大量的数据预取和缓存。您可能会以较慢的迭代顺序生成许多数据。

您可以添加一些基准测试结果吗?相关:@NOTT101基准测试将显示3到10倍的性能差异。这是基本的C/C++,我是completely被问到这是如何获得这么多选票的…@TC1:我不认为这是基本的,也许是中间的。但“基本”的东西对更多的人来说是有用的,这也就不足为奇了,因此会有很多人投票。此外,这是一个谷歌很难回答的问题,即使它是“基本的”。这是一个非常彻底的答案;我
  for (j=0; j<4000; j++) {
    int *p = x[j];
    for (i=0; i<4000; i++) {
      *p++ = i+j;
    }
  }