在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;
}
}
答案 0 :(得分:64)
第一种方法稍好一些,因为被指定的单元格彼此相邻。
第一种方法:
[ ][ ][ ][ ][ ] ....
^1st assignment
^2nd assignment
[ ][ ][ ][ ][ ] ....
^101st assignment
第二种方法:
[ ][ ][ ][ ][ ] ....
^1st assignment
^101st assignment
[ ][ ][ ][ ][ ] ....
^2nd assignment
答案 1 :(得分:43)
对于数组[100] [100] - 它们都是相同的,如果L1缓存大于100 * 100 * sizeof(int)== 10000 * sizeof(int)== [通常] 40000在Sandy Bridge中注意 - 100 * 100个整数应该足以看到差异,因为L1缓存只有32k。
编译器可能会优化此代码
假设没有编译器优化,并且矩阵不适合L1缓存 - 由于缓存性能[通常],第一个代码更好。每次在缓存中找不到元素时 - 你得到cache miss - 并且需要转到RAM或L2缓存[这要慢得多]。从RAM到缓存[缓存填充]的元素以块[通常为8/16字节]完成 - 因此在第一个代码中,您获得最多未命中率1/4
[假设为16字节高速缓存块,4字节整数]而在第二个代码中它是无界的,甚至可以是1.在第二个代码快照中 - 已经在高速缓存中的元素[插入到相邻元素的高速缓存填充中] - 被取出,你得到一个冗余缓存未命中。
<强>结论:强> 对于我所知道的所有缓存实现 - 第一个将不会比第二个更糟糕。它们可能是相同的 - 如果根本没有缓存或者所有数组完全适合缓存 - 或者由于编译器优化。
答案 2 :(得分:13)
这种微优化与平台有关,因此您需要对代码进行分析,以便能够得出合理的结论。
答案 3 :(得分:10)
在您的第二个片段中,每次迭代中j
的更改会生成一个空间局部性较低的模式。请记住,在幕后,数组引用计算:
( ((y) * (row->width)) + (x) )
考虑一个简化的L1缓存,它只有50行我们的数组有足够的空间。对于前50次迭代,您将支付50次缓存未命中的不可避免的成本,但接下来会发生什么?对于从50到99的每次迭代,您仍将缓存未命中并且必须从L2(和/或RAM等)获取。然后,x
更改为1并且y
重新开始,导致另一个缓存未命中,因为数组的第一行已从缓存中逐出,等等。
第一个代码段没有此问题。它以行主要顺序访问数组,从而实现更好的局部性 - 您只需要为最多一次缓存未命中付费(如果您的数组中的某行不存在于缓存中)循环开始)每行。
话虽这么说,这是一个非常依赖于架构的问题,因此您必须考虑具体情况(L1缓存大小,缓存行大小等)来得出结论。您还应该测量两种方式并跟踪硬件事件,以获得具体数据以从中得出结论。
答案 4 :(得分:5)
考虑到C ++是行专业,我相信第一种方法会更快一些。在内存中,2D数组在单维数组中表示,性能取决于使用行主列或列主要
访问它答案 5 :(得分:4)
这是关于cache line bouncing
大多数时候第一个更好,但我认为确切的答案是: IT DEPENDS ,不同的架构可能会有不同的结果。
答案 6 :(得分:4)
在第二种方法中,缓存未命中,因为缓存存储了连续数据。 因此第一种方法比第二种方法有效。
答案 7 :(得分:3)
在你的情况下(填充所有数组1的值),这将更快:
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];
}
答案 8 :(得分:2)
一般来说,更好的位置(大多数响应者注意到)只是循环#1性能的第一个优势。
第二个(但相关的)优点是循环#1 - 编译器通常能够使用stride-1内存访问有效地自动矢量化代码pattern(stride-1表示在每次下一次迭代中都会逐个连续访问数组元素)。 相反, for for like(如#2 ),自动矢量化通常不会正常工作,因为内存中没有连续的stride-1迭代访问contiguos块。
嗯,我的回答很一般。对于非常简单的循环(如#1或#2),可能会使用更简单的激进编译器优化(对任何差异进行分级),编译器通常也能够使用stride-1自动向量化#2 对于外部循环(特别是使用#pragma simd或类似的东西)。
答案 9 :(得分:1)
第一个选项更好,因为我们可以将a[i] in a temp variable
存储在第一个循环中,然后在其中查找j索引。从这个意义上讲,它可以说是缓存变量。