我最近注意到,在C中访问矩阵的方式看似微不足道的变化会对性能产生很大影响。 例如,让我们想象我们有这两个C代码片段。这一个:
for(i = 0; i < 2048; i++)
{
for(j = 0; j < 2048; j++) {
Matrix[i][j] = 9999;
}
}
这一个:
for(j = 0; j < 2048; j++)
{
for(i = 0; i < 2048; i++) {
Matrix[i][j] = 9999;
}
}
第二个版本比第一个版本慢2倍。为什么?我认为它与内存管理有关:在每个循环中,第一个版本访问内存中彼此相邻的位置,而第二个版本必须&#34;跳转&#34;每个循环中的不同区域。 这种直觉是对的吗? 此外,如果我使矩阵变小(例如64x64),那么性能没有差别。为什么? 如果有人能提供直观而严谨的解释,我将不胜感激。 顺便说一下,我正在使用Ubuntu 14.04 LTS。
答案 0 :(得分:5)
for(i=0;i<2048;i++)
{
for(j=0;j<2048;j++) {
Matrix[i][j]=9999;
}
}
此表单使用L1,L2和L3缓存对齐。当您使用j
循环Matrix[i][j]
时,元素Matrix[i][0]
,Matrix[i][1]
... a.s.o。在连续的地址处对齐(实际上在sizeof(Matrix[i][0]))
的地址不同,因此访问Matrix[i][0]
会在缓存中引入下一个变量Matrix [i] [1]。
另一方面,
for(j=0;j<2048;j++)
{
for(i=0;i<2048;i++) {
Matrix[i][j]=9999;
}
}
内部循环以Matrix[0][j]
,Matrix[1][j]
... a.s.o的顺序访问。 Matrix[1][j]
的地址为Matrix[0][j]+2048*sizeof(Matrix[0][0])
- 假设您为数组Matrix[0]
分配了2048个条目。
所以Matrix[0][j]
位于另一个缓存块而不是Matrix[1][j]
,要求提取在RAM中进行访问而不是缓存。
在第二种情况下,每次迭代都可以访问RAM。
答案 1 :(得分:3)
&#34; 它是缓存!它是缓存!&#34;
要想象它,将内存视为线性数组...
通过定义2D数组:
uint8_t Matrix[4][4]
你只是说:
allocate 16 bytes, and access them as a 2D array, 4x4
这个例子假设一个4字节的缓存,简单起见:
如果CPU的缓存只能容纳4个字节,那么接近[0][0]
,[1][0]
,[2][0]
,...表单中的数组将导致缓存未命中每次访问 - 要求我们访问RAM(这是昂贵的)16次!
接近[0][0]
,[0][1]
,[0][2]
,...表单中的数组将允许完整访问2D数组,只有4个缓存未命中。
这个例子非常简单 - 现代系统几乎肯定会有一个L1和L2缓存,而且很多人现在也在实现L3缓存。
随着处理器内核越来越远,内存越来越大。例如:
答案 2 :(得分:2)
它与locality of reference和CPU cache相关。所以它主要是针对特定处理器的(并没有特定的操作系统)。
高速缓存未命中可能非常昂贵(typically,对DRAM模块上的数据的访问需要数百纳秒 - 足以从L1 I-cache执行一百个机器指令),但是对L1高速缓存的访问只需要一个或者几纳秒)。
另请阅读this和that。有时(但并非总是)使用__builtin_prefetch
可能可以提高性能(但通常GCC编译器可以通过适当地发出PREFETCH
machine instructions来优化您的优化。但严重或过于频繁地使用__builtin_prefetch
会损害性能。
不要忘记在编译器中启用优化,因此至少在基准测试之前使用gcc -Wall -O2 -march=native
进行编译(甚至是-O3
而不是-O2
...)。
答案 3 :(得分:2)
关于缓存的全部内容。在后者中,您基本上是按顺序读取内存。在第一种情况下,你会在每次阅读之间做很长的跳跃。
计算机中存在将附近数据存储在读取中的电路,因为很可能很快就会读取附近的数据。您无法控制这些电路的工作方式。您所能做的就是根据行为调整代码。