我有一个数组long matrix[8*1024][8*1024]
和两个函数sum1
和sum2
:
long sum1(long m[ROWS][COLS]) {
long register sum = 0;
int i,j;
for (i=0; i < ROWS; i++) {
for (j=0; j < COLS; j++) {
sum += m[i][j];
}
}
return sum;
}
long sum2(long m[ROWS][COLS]) {
long register sum = 0;
int i,j;
for (j=0; j < COLS; j++) {
for (i=0; i < ROWS; i++) {
sum += m[i][j];
}
}
return sum;
}
当我使用给定的数组执行这两个函数时,我得到了运行时间:
sum1:0.19秒
sum2:1.25秒
谁能解释为什么会有如此巨大的差异?
答案 0 :(得分:25)
C使用行优先排序来存储多维数组,如C标准的§ 6.5.2.1 Array subscripting, paragraph 3中所述:
成功的下标运算符指定多维数组对象的元素。如果E是尺寸为i x j x的n维数组(n> = 2)。 。 。 x k,然后将E(用作左值)转换为指向维度为j x的(n-1)维数组的指针。 。 。则如果将一元*运算符显式或间接地应用于下标,则结果是被引用的(n-1)维数组,如果用作左值以外的数组,则其本身将转换为指针。 据此,数组以行优先顺序存储(最后一个下标变化最快)。
强调我的。
这是Wikipedia的图像,与其他用于存储多维数组的方法,列主排序
第一个函数sum1
按照实际在内存中表示二维数组的方式连续访问数据,因此来自数组的数据已经在缓存中。 sum2
要求每次迭代都提取另一行,因此不太可能出现在缓存中。
还有其他一些语言对多维数组使用列优先顺序;其中有R,FORTRAN和MATLAB。如果您使用这些语言编写了等效的代码,则sum2
的输出会更快。
答案 1 :(得分:19)
计算机通常使用 cache 来帮助加快对主内存的访问。
通常用于主存储器的硬件相对较慢-数据从主存储器到处理器的处理可能需要很多处理器周期。因此,计算机通常包括少量的非常快速但昂贵的内存,称为高速缓存。计算机可能具有多个级别的缓存,其中一些缓存内置在处理器或处理器芯片本身中,而某些则位于处理器芯片之外。
由于缓存较小,因此无法将所有内容保存在主内存中。它通常甚至无法保存一个程序正在使用的所有内容。因此,处理器必须对缓存中保留的内容进行决策。
对程序的最频繁访问是访问内存中的连续位置。很多时候,程序读取数组的元素237之后,很快就会读取238,然后是239,依此类推。读取237之后,读取7024的频率降低了。
因此,缓存操作旨在将连续的主内存部分保留在缓存中。您的sum1
程序可以很好地解决此问题,因为它可以最快速地更改列索引,并在处理所有列时保持行索引不变。它访问的数组元素在内存中连续布置。
您的sum2
程序不能很好地与此配合,因为它最快速地更改了行索引。这会在内存中跳来跳去,因此它进行的许多访问都无法由缓存满足,而必须来自较慢的主内存。
答案 2 :(得分:4)
在具有数据缓存的机器上(即使68030拥有一个),在连续的内存位置中读取/写入数据的速度也更快,这是因为从内存中提取了一次内存块(大小取决于处理器)然后从中调用缓存(读取操作)或一次写入(缓存刷新以进行写入操作)。
通过“跳过”数据(读取的数据与先前读取的数据相差很远),CPU必须再次读取内存。
这就是为什么您的第一个片段更快。
对于更复杂的操作(例如快速傅立叶变换),数据被读取一次以上(不同于您的示例),许多库(例如FFTW)建议使用 stride 来适应您的数据组织(按行/按列)。 从不使用它,始终先转置数据并使用跨度为1,这比尝试不进行转置要快得多。
为确保数据连续,请不要使用2D表示法。首先将数据放置在所选行中,然后将指针设置到该行的开头,然后在该行上使用内部循环。
for (i=0; i < ROWS; i++) {
const long *row = m[i];
for (j=0; j < COLS; j++) {
sum += row[j];
}
}
如果您无法执行此操作,则意味着您的数据方向错误。
答案 3 :(得分:3)
这是缓存问题。
缓存将自动读取位于您请求的数据之后的数据。因此,如果您逐行读取数据,那么您请求的下一个数据将已经在缓存中。
答案 4 :(得分:3)
内存中的矩阵是线性对齐的,因此一行中的项目在内存中彼此相邻(spacial locality
)。当您按顺序对项目进行排序以便在移动到下一列之前先经过一行中的所有列时,当CPU遇到尚未加载到其缓存中的条目时,它将沿着该行加载该值物理内存中还有其他接近整个值的块,因此接下来的几个值将在需要读取它们时被缓存。
以另一种方式横向访问它们时,它加载的其他值在内存中接近它的值将不会被下一个读取,因此您最终会有更多的缓存未命中,因此CPU必须坐下来然后等待数据从内存层次结构的下一层引入。
当您回滚到先前已缓存的另一个条目时,它很可能已经从缓存中引导出来,以支持自此以来已加载的所有其他数据,因为最近不会不再使用(temporal locality
)
答案 5 :(得分:2)
要扩展其他答案,这是由于第二个程序的缓存丢失所致,并且假设您使用的是Linux,* BSD或MacOS,那么Cachegrind可能会给您启发。它是valgrind的一部分,将在不做任何更改的情况下运行您的程序,并打印缓存使用情况统计信息。它的运行速度确实很慢。