在时间(缓存性能)方面,以下哪种嵌套循环的顺序来遍历2D数组更有效?为什么?
int a[100][100];
for(i=0; i<100; i++)
{
for(j=0; j<100; j++)
{
a[i][j] = 10;
}
}
或者for(i=0; i<100; i++)
{
for(j=0; j<100; j++)
{
a[j][i] = 10;
}
}
在时间(缓存性能)方面,以下哪种嵌套循环的顺序来遍历2D数组更有效?为什么?
int a[100][100];
for(i=0; i<100; i++)
{
for(j=0; j<100; j++)
{
a[i][j] = 10;
}
}
或者for(i=0; i<100; i++)
{
for(j=0; j<100; j++)
{
a[j][i] = 10;
}
}
第一种方法稍微好些,因为被分配的单元格都在相邻位置。
第一种方法:
[ ][ ][ ][ ][ ] ....
^1st assignment
^2nd assignment
[ ][ ][ ][ ][ ] ....
^101st assignment
第二种方法:
[ ][ ][ ][ ][ ] ....
^1st assignment
^101st assignment
[ ][ ][ ][ ][ ] ....
^2nd assignment
对于数组[100][100] - 如果L1缓存比100*100*sizeof(int) == 10000*sizeof(int) == [通常] 40000要大,则它们是相同的。需要注意的是,在Sandy Bridge中- 100*100个整数应该足够看到差异,因为L1缓存只有32k。
编译器可能会优化这段代码。
假设没有编译器优化,并且矩阵不适合L1缓存-由于缓存性能通常较好,第一段代码更好。每次在缓存中找不到元素时 - 您会得到一个缓存未命中 - 并且需要访问RAM或L2缓存[速度要慢得多]。将元素从RAM转移到高速缓存中[高速缓存填充]是以块[通常8/16字节]进行的 - 所以在第一段代码中,您获得了1/4
的最大未命中率[假设16字节缓存块,4字节整数],而在第二段代码中,它是不受限制的,甚至可以达到1。在第二段代码快照中 - 已经在高速缓存填充中插入的元素[为相邻元素]将被取出,这样您就会得到冗余的缓存未命中。
结论: 对于我所知道的所有缓存实现-第一个比第二个更好。如果没有缓存或整个数组完全适合缓存,它们可能相同-或者由于编译器优化。
1/4
和 1
的呢? - sgnsajgon这种微小的优化取决于平台,因此您需要对代码进行剖析以得出合理结论。
j
的变化会产生一个具有低空间局部性的模式。请记住,在幕后,数组引用计算的是:( ((y) * (row->width)) + (x) )
考虑一个简化的 L1 缓存,它只有足够空间来容纳数组的 50 行。在前 50 次迭代中,你将支付 50 次不可避免的缓存未命中成本,但是接下来会发生什么呢?从第 50 次到第 99 次的每次迭代中,你仍然会出现缓存未命中并且必须从 L2(和/或 RAM 等)获取。然后,x
改变为 1,y
重新开始,导致另一个缓存未命中,因为数组的第一行已经被驱逐出缓存,等等。考虑到C++是行主序,我认为第一种方法会稍微快一些。在内存中,二维数组表示为单维数组,性能取决于使用行主序还是列主序进行访问。
这是一个关于缓存行反弹的经典问题。
大多数情况下,第一种方法更好,但我认为确切的答案是:取决于具体情况,不同的架构可能会有不同的结果。
for(j = 0; j < 100 * 100; j++){
a[j] = 10;
}
你仍然可以将a
视为二维数组。
编辑:正如Binyamin Sharet所提到的,如果你的a
是这样声明的,你就可以这样做:
int **a = new int*[100];
for(int i = 0; i < 100; i++){
a[i] = new int[100];
}
总的来说,更好的局部性(大多数回答者都注意到了)只是循环#1性能的第一个优点。
第二个(但相关的)优点是,对于像循环#1这样的循环,编译器通常能够使用步幅为1的内存访问模式(步幅为1表示在每个下一次迭代中连续访问数组元素),从而有效地自动向量化代码。相反,对于像循环#2这样的循环,自动向量化通常不会很好地工作,因为在内存中没有连续的步幅为1的迭代访问连续块。
嗯,我的答案是普遍的。对于像#1或#2这样的非常简单的循环,可能会使用更简单的激进编译器优化(分级任何差异),并且编译器通常能够使用步幅为1的自动向量化外部循环#2(特别是使用#pragma simd或类似方法)。
第一种选项更好,因为我们可以在第一个循环中将 a[i] 存储在临时变量
中,然后在其中查找 j 索引。从这个意义上说,它可以被称为缓存变量。