以下两个程序几乎完全相同,只是我切换了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; }
}
}
第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; }
}
}
答案 0 :(得分:564)
正如其他人所说,问题是存储到数组中的内存位置: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...
意味着你按顺序击中它们。现在看第一个版本。你在做:
x[0][0]
x[1][0]
x[2][0]
x[0][1]
x[1][1] etc...
由于C在内存中布置2-d数组的方式,你要求它在整个地方跳跃。但现在对于踢球者:为什么这很重要?所有内存访问都是一样的,对吗?
否:因为缓存。来自内存的数据以小块(称为“缓存行”)传递给CPU,通常为64字节。如果你有4字节的整数,那意味着你要在一个整齐的小包中找到16个连续的整数。获取这些内存块实际上相当慢;您的CPU可以在加载单个缓存行所需的时间内完成大量工作。
现在回顾一下访问的顺序:第二个例子是(1)抓取一个16个整数的块,(2)修改所有这些,(3)重复4000 * 4000/16次。这很好而且速度很快,而且CPU总是可以使用。
第一个例子是(1)抓取一个16个整数的块,(2)只修改其中一个,(3)重复4000 * 4000次。这将需要16倍于内存中“提取”的数量。你的CPU实际上必须花时间坐在那里等待记忆显示出来,而当它坐在你周围时你会浪费宝贵的时间。
重要提示:
现在你已经得到了答案,这里有一个有趣的说明:你的第二个例子必须是快速的,没有固有的原因。例如,在Fortran中,第一个例子很快,第二个例子很慢。这是因为Fortran不是像C那样将事物扩展为概念性的“行”,而是扩展为“列”,即:
0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3
C的布局称为'row-major',Fortran称为'column-major'。正如您所看到的,了解您的编程语言是行主要还是列专业是非常重要的!以下是更多信息的链接:http://en.wikipedia.org/wiki/Row-major_order
答案 1 :(得分:64)
与装配无关。这是由于cache misses。
存储C多维数组,最后一个维度为最快。因此,第一个版本将在每次迭代时错过缓存,而第二个版本则不会。所以第二个版本应该快得多。
答案 2 :(得分:22)
版本2的运行速度要快得多,因为它比版本1更好地使用计算机的缓存。如果你考虑它,数组就只是连续的内存区域。当您在数组中请求元素时,您的操作系统可能会将内存页面引入包含该元素的缓存中。但是,由于接下来的几个元素也在该页面上(因为它们是连续的),下一次访问将已经在缓存中!这就是版本2正在做的事情,以加快它的速度。
另一方面,版本1是按列访问元素,而不是按行访问。这种访问在内存级别上并不连续,因此程序无法充分利用操作系统缓存。
答案 3 :(得分:12)
原因是缓存本地数据访问。在第二个程序中,您将通过内存线性扫描,这有助于缓存和预取。您的第一个程序的内存使用模式更加分散,因此缓存行为更差。
答案 4 :(得分:10)
除了缓存命中的其他优秀答案之外,还存在可能的优化差异。您的第二个循环可能会被编译器优化为等同于:
的东西 for (j=0; j<4000; j++) {
int *p = x[j];
for (i=0; i<4000; i++) {
*p++ = i+j;
}
}
这对第一个循环的可能性较小,因为每次需要增加4000个指针“p”。
编辑: p++
甚至*p++ = ..
可以编译为大多数CPU中的单个CPU指令。 *p = ..; p += 4000
不能,因此优化它的好处较少。它也更难,因为编译器需要知道并使用内部数组的大小。并且通常在正常代码的内部循环中不会发生(它仅出现在多维数组中,其中最后一个索引在循环中保持不变,而倒数第二个索引是步进的),因此优化不是优先级。
答案 5 :(得分:7)
这一行是罪魁祸首:
x[j][i]=i+j;
第二个版本使用连续内存,因此速度会快得多。
我试过
x[50000][50000];
版本1的执行时间为13秒,版本2的执行时间为0.6秒。
答案 6 :(得分:4)
我尝试给出一般答案。
因为i[y][x]
是C中*(i + y*array_width + x)
的简写(尝试优等int P[3]; 0[P] = 0xBEEF;
)。
在迭代y
时,您会迭代大小为array_width * sizeof(array_element)
的块。如果你在内循环中有那个,那么你将在这些块上进行array_width * array_height
次迭代。
通过翻转顺序,您将只有array_height
块迭代,并且在任何块迭代之间,您将只有array_width
的{{1}}次迭代。
虽然在真正老的x86-CPU上这并不重要,但现在'x86做了很多预取和数据缓存。您可能会在较慢的迭代次序中生成许多cache misses。