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)
的简写形式(试试classyint 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;
}
}