我的考试主题是程序设计语言,我遇到了一个问题。我想了很久但我还是不明白这个问题
问题: 下面是一个程序C,它在PC上的MSVC ++ 6.0环境中执行,配置为~CPU Intel 1.8GHz,Ram 512MB
#define M 10000
#define N 5000
int a[M][N];
void main() {
int i, j;
time_t start, stop;
// Part A
start = time(0);
for (i = 0; i < M; i++)
for (j = 0; j < N; j++)
a[i][j] = 0;
stop = time(0);
printf("%d\n", stop - start);
// Part B
start = time(0);
for (j = 0; j < N; j++)
for (i = 0; i < M; i++)
a[i][j] = 0;
stop = time(0);
printf("%d\n", stop - start);
}
解释为什么A部分只在 1s 中执行,但B 8s 完成了?
答案 0 :(得分:21)
这与数组内存的布局方式以及如何将其加载到缓存中并进行访问有关:在版本A中,当访问数组的单元格时,邻居会将其加载到缓存中,并且代码然后立即访问这些邻居。在版本B中,访问一个单元格(并将其邻居加载到缓存中),但下一行访问距离很远,在下一行,因此整个缓存行已加载但只使用了一个值,另一个缓存行必须为每次访问填写。因此速度差异。
答案 1 :(得分:12)
行主要订单与列主要订单。
首先回想一下,所有多维数组都在内存中表示为连续的内存块。因此,多维数组A(m,n)可以在存储器中表示为
a00 a01 a02 ... a0n a10 a11 a12 ... a1n a20 ... amn
在第一个循环中,按顺序运行此内存块。因此,您按照以下顺序遍历遍历元素的数组
a00 a01 a02 ... a0n a10 a11 a12 ... a1n a20 ... amn
1 2 3 n n+1 n+2 n+3 ... 2n 2n+1 mn
在第二个循环中,您在内存中跳过并按以下顺序遍历遍历元素的数组
a00 a10 a20 ... am0 a01 a11 a21 ... am1 a02 ... amn
或者,或许更清楚,
a00 a01 a02 ... a10 a11 a12 ... a20 ... amn
1 m+1 2m+1 2 m+2 2m+2 3 mn
所有跳过的东西真的会伤害你,因为你没有从缓存中获益。按顺序运行数组时,相邻元素将加载到缓存中。当您跳过数组时,您不会获得这些好处,而是继续获得缓存未命中,从而损害性能。
答案 2 :(得分:6)
由于硬件架构优化。 A部分正在对顺序存储器地址执行操作,这允许硬件大大加速计算的处理方式。 B部分基本上都是在内存中跳转,这会使许多硬件优化失败。
此特定案例的关键概念是processor cache。
答案 3 :(得分:6)
您声明的数组在内存中按行排列。基本上你有一大块M×N整数,C做了一些小技巧让你相信它是矩形的。但实际上它是平的。
因此,当你逐行迭代(使用M作为外部循环变量)时,你实际上是通过内存线性地进行的。 CPU缓存处理得非常好。
但是,当你在外部循环中使用N进行迭代时,你总是在内存中进行或多或少的随机跳转(至少对于它看起来像这样的硬件)。您正在访问第一个单元格,然后进一步移动M个整数并执行相同操作等。由于您的内存页面通常大约为4 KiB,这会导致另一个页面被访问每次迭代内循环。这样几乎任何缓存策略都会失败,你会看到一个重大的放缓。
答案 4 :(得分:1)
麻烦就在这里,你的数组如何在内存中存放。
在计算机内存中,通常会分配数组,例如,第一行的所有列首先出现,然后是第二行的所有列,依此类推。
您的计算机内存最好被视为一长串字节 - 它是一维的内存数组 - 而不是二维数据,因此必须以所描述的方式分配多维数组。
现在出现了另一个问题:现代CPU有缓存。它们有多个缓存,并且它们具有用于第一级缓存的所谓“缓存行”。这是什么意思。访问内存很快,但速度不够快。现代CPU更快。所以他们有自己的片上缓存,可以加快速度。此外,它们不再访问单个内存位置,但它们在一次获取中填充一个完整的缓存行。这也是为了表现。但是这种行为给出了线性处理数据的所有操作优势。当您首先访问一行中的所有列,然后访问下一行,依此类推 - 您实际上是线性工作的。当您第一次处理所有行的所有第一列时,您将“跳转”到内存中。因此,您始终强制填充新的缓存行,只需处理几个字节,然后您的下一次跳转可能会使缓存行无效....
因此,对于现代处理器来说,列主要顺序是不好的,因为它不能线性工作。