我经历了循环,发现访问循环有很大的不同。 我无法理解在两种情况下造成这种差异的原因是什么?
第一个例子:
执行时间; 8秒
for (int kk = 0; kk < 1000; kk++)
{
sum = 0;
for (int i = 0; i < 1024; i++)
for (int j = 0; j < 1024; j++)
{
sum += matrix[i][j];
}
}
第二个例子:
执行时间:23秒
for (int kk = 0; kk < 1000; kk++)
{
sum = 0;
for (int i = 0; i < 1024; i++)
for (int j = 0; j < 1024; j++)
{
sum += matrix[j][i];
}
}
导致如此多的执行时间差异只是交换
matrix[i][j]
到
matrix[j][i]
答案 0 :(得分:111)
这是内存缓存的问题。
matrix[i][j]
的缓存命中率高于matrix[j][i]
,因为matrix[i][j]
具有更多的连续内存访问机会。
例如,当我们访问matrix[i][0]
时,缓存可能会加载包含matrix[i][0]
的连续内存段,从而访问matrix[i][1]
,matrix[i][2]
,...,将受益于缓存速度,因为matrix[i][1]
,matrix[i][2]
,...靠近matrix[i][0]
。
但是,当我们访问matrix[j][0]
时,它远离matrix[j - 1][0]
并且可能未被缓存,并且无法从缓存速度中受益。特别是,矩阵通常存储为一个连续的大段内存,而cacher可以预测内存访问的行为并始终缓存内存。
这就是matrix[i][j]
更快的原因。这在基于CPU缓存的性能优化中是典型的。
答案 1 :(得分:54)
性能差异是由计算机的缓存策略引起的。
二维数组matrix[i][j]
表示为内存中的一长串值。
例如,数组A[3][4]
如下:
1 1 1 1 2 2 2 2 3 3 3 3
在此示例中,A [0] [x]的每个条目都设置为1,A [1] [x]的每个条目都设置为2,...
如果您的第一个循环应用于此矩阵,则访问顺序为:
1 2 3 4 5 6 7 8 9 10 11 12
虽然第二个循环访问顺序如下所示:
1 4 7 10 2 5 8 11 3 6 9 12
当程序访问数组的元素时,它还会加载后续元素。
E.g。如果您访问A[0][1]
,则也会加载A[0][2]
和A[0][3]
。
因此,第一个循环必须执行较少的加载操作,因为某些元素在需要时已经在缓存中。 第二个循环将条目加载到当时不需要的缓存中,从而导致更多的加载操作。
答案 2 :(得分:34)
其他人已经很好地解释了为什么一种形式的代码比另一种形式更有效地使用内存缓存。我想添加一些您可能不知道的背景信息:您可能没有意识到现在主要的内存访问费用是多少。
this question中发布的数字看起来对我来说是正确的,我会在这里重现它们,因为它们非常重要:
Core i7 Xeon 5500 Series Data Source Latency (approximate)
L1 CACHE hit, ~4 cycles
L2 CACHE hit, ~10 cycles
L3 CACHE hit, line unshared ~40 cycles
L3 CACHE hit, shared line in another core ~65 cycles
L3 CACHE hit, modified in another core ~75 cycles remote
remote L3 CACHE ~100-300 cycles
Local Dram ~60 ns
Remote Dram ~100 ns
请注意最后两个条目的单位变化。根据您的具体型号,该处理器的运行频率为2.9-3.2 GHz;为了简化数学运算,我们只需称它为3 GHz。所以一个周期是0.33333纳秒。因此DRAM访问也是100-300个周期。
重点是CPU在从主内存读取一个缓存行所花费的时间内可能已经执行了数百个指令。这称为memory wall。因此,在现代CPU的整体性能中,有效使用内存缓存比任何其他因素更重要。
答案 3 :(得分:18)
答案取决于matrix
的确切定义。在完全动态分配的数组中,您有:
T **matrix;
matrix = new T*[n];
for(i = 0; i < n; i++)
{
t[i] = new T[m];
}
因此,每个matrix[j]
都需要对指针进行新的内存查找。如果在外部执行j
循环,则内循环可以为整个内循环重复使用matrix[j]
的指针。
如果矩阵是一个简单的二维数组:
T matrix[n][m];
然后matrix[j]
只是乘以1024 * sizeof(T)
- 这可以通过在优化代码中添加1024 * sizeof(T)
循环索引来完成,因此无论如何都应该相对较快。
最重要的是,我们有缓存局部因素。高速缓存有&#34;线&#34;数据通常为每行32到128个字节。因此,如果您的代码读取地址X
,则缓存将加载X
周围32到128个字节的值。因此,如果您需要的NEXT事物仅从当前位置sizeof(T)
转发,它很可能已经在缓存中[并且现代处理器也会检测到您正在循环读取每个内存位置,并预加载数据]。
在j
内循环的情况下,您正在为每个循环读取sizeof(T)*1024
距离的新位置[或者如果它被动态分配,则可能是更大的距离]。这意味着正在加载的数据对下一个循环没有用,因为它不在接下来的32到128个字节中。
最后,由于SSE指令或类似指令,第一个循环可以更加优化,这使得计算能够更快地运行。但这对于如此大的矩阵来说可能是微不足道的,因为性能在这个大小上具有很高的内存限制。
答案 4 :(得分:10)
内存硬件未针对提供单个地址进行优化:相反,它倾向于在称为缓存行的更大块连续内存上运行。每次读取矩阵的一个条目时,它所在的整个缓存行也会随之加载到缓存中。
设置更快的循环排序以按顺序读取内存;每次加载缓存行时,都使用该缓存行中的所有条目。每次通过外部循环,您只需一次读取每个矩阵条目。
然而,较慢的循环排序仅在继续之前使用来自每个缓存行的单个条目。因此,每个高速缓存行必须多次加载,对于行中的每个矩阵条目一次。例如如果double
是8个字节且高速缓存行长度为64个字节,那么每次通过外部循环必须读取每个矩阵条目 8个次而不是一次。
所有这一切,如果你已经开启优化,你可能会看到没有区别:优化者理解这种现象,好的能够识别他们可以交换哪个循环是内循环,哪个循环是外循环对于此特定代码段。
(另外,一个优秀的优化器只能通过最外层循环完成一次,因为它识别前999次通过与sum
的最终值无关)
答案 5 :(得分:8)
矩阵作为向量存储在存储器中。以第一种方式访问它顺序访问内存。以第二种方式访问它需要跳过内存位置。见http://en.wikipedia.org/wiki/Row-major_order
答案 6 :(得分:5)
如果你访问j - i,j维度被缓存,所以机器代码不必每次都改变它,第二个维度没有被缓存,所以你实际上每次都会删除缓存。这会导致差异。
答案 7 :(得分:3)
基于引用局部性的概念,一段代码很可能访问相邻的内存位置。因此,更多的值被加载到缓存中而不是所要求的值。这意味着更多缓存命中。你的第一个例子是满足这个,而第二个例子中的代码不是。