为什么逐列复制2D数组比C中的逐行复制?

时间:2015-12-15 23:22:33

标签: c arrays

#include <stdio.h>
#include <time.h>

#define N  32768

char a[N][N];
char b[N][N];

int main() {
    int i, j;

    printf("address of a[%d][%d] = %p\n", N, N, &a[N][N]);
    printf("address of b[%5d][%5d] = %p\n", 0, 0, &b[0][0]);

    clock_t start = clock();
    for (j = 0; j < N; j++)
        for (i = 0; i < N; i++)
            a[i][j] = b[i][j];
    clock_t end = clock();
    float seconds = (float)(end - start) / CLOCKS_PER_SEC;
    printf("time taken: %f secs\n", seconds);

    start = clock();
    for (i = 0; i < N; i++)
        for (j = 0; j < N; j++)
            a[i][j] = b[i][j];
    end = clock();
    seconds = (float)(end - start) / CLOCKS_PER_SEC;
    printf("time taken: %f secs\n", seconds);

    return 0;
}

输出:

address of a[32768][32768] = 0x80609080
address of b[    0][    0] = 0x601080
time taken: 18.063229 secs
time taken: 3.079248 secs

为什么逐列复制几乎是逐行复制的6倍?我知道2D数组基本上是一个nxn大小的数组,其中A [i] [j] = A [i * n + j],但是使用简单的代数,我计算出一个图灵机头(在主内存上)必须要去旅行两种情况下的距离为enter image description here。这里nxn是数组的大小,x是第一个数组的最后一个元素和第二个数组的第一个元素之间的距离。

3 个答案:

答案 0 :(得分:14)

这几乎归结为这张图片(source):

enter image description here

访问数据时,CPU不仅会加载单个值,还会将相邻数据加载到CPU的L1缓存中。在逐行迭代数组时,自动加载到缓存中的项实际上是接下来处理的项。但是,当您按列进行迭代时,每次加载整个“缓存行”数据(每个CPU的大小不同)时,只使用一个项目,然后必须加载下一行,从而有效地使缓存无意义

wikipedia entry以及this PDF作为高级概述,可以帮助您了解CPU缓存的工作原理。

编辑:评论中的chqrlie当然是正确的。这里的一个相关因素是,只有极少数列同时适合L1缓存。如果您的行要小得多(比如,二维数组的总大小只有几千字节),那么您可能看不到迭代每列的性能影响。

答案 1 :(得分:4)

虽然将数组绘制为矩形是正常的,但内存中数组元素的寻址是线性的:0到1减去可用的字节数(几乎在所有机器上)。

存储器层次结构(例如,寄存器&lt; L1高速缓存&lt; L2高速缓存&lt; RAM&lt;磁盘上的交换空间)针对存储器访问被本地化的情况进行了优化:接近时间的访问触摸地址靠近在一起。它们甚至被更高度优化(例如,具有预取策略),用于以地址的线性顺序进行顺序访问;例如100101102 ...

在C中,矩形数组通过连接所有行(其他语言,如FORTRAN和Common Lisp连接列)以线性顺序排列。因此,读取或写入数组的最有效方法是执行第一行的所有列,然后逐行移动到其余列。

如果你改为列,则连续触摸相隔N个字节,其中N是一行中的字节数:100,10100,20100,30100 ...对于N = 10000字节的情况。然后是第二个列是101,10101,20101等。这是大多数缓存方案的绝对最坏情况。

在最糟糕的情况下,您可能会在每次访问时导致页面错误。这些天来,即使在普通的机器上也需要大量的工作才能实现。但如果它发生了,每次触摸可能需要大约10毫秒的头部搜索。顺序访问每秒几纳秒。这超过了百万差异的因素。在这种情况下,计算有效地停止。它有一个名字:disk thrashing。

在更常见的情况下,只涉及缓存故障,而不是页面错误,您可能会看到百分之一。还是值得关注。

答案 2 :(得分:1)

有三个主要方面导致时间不同:

  1. 第一个双循环首次访问两个数组。你实际上正在读取未初始化的内存,如果你期望任何有意义的结果(在功能上和时序上)是坏的,但就时间而言,这里的部分是这些地址是冷的,并且驻留在主存储器中(如果你很幸运,或者甚至没有被分页(如果你不那么幸运)。在后一种情况下,每个新页面都会出现页面错误,并且会调用系统调用来首次分配页面。请注意,这与遍历的顺序没有任何关系,只是因为第一次访问速度要慢得多。为避免这种情况,请将两个数组初始化为某个值。

  2. 缓存行位置(如其他答案中所述) - 如果访问顺序数据,则每行丢失一次,然后享受已经获取它的好处。您很可能甚至不会在缓存中命中它,而是在某个缓冲区中,因为连续的请求将等待该行被提取。在按列访问时,您将获取该行,对其进行缓存,但如果重用距离足够大 - 您将丢失它并且必须再次获取它。

  3. 预取 - 现代CPU将具有硬件预取机制,可以检测顺序访问并提前预取数据,这将消除每行的第一次丢失。大多数CPU也有基于步幅的预取,这些预取可能能够覆盖列大小,但是这些东西通常不能很好地与矩阵结构一起工作,因为你有太多的列,并且HW不可能同时跟踪所有这些步幅流。

  4. 作为旁注,我建议任何时间测量都要多次执行并摊销 - 这样就可以解决问题#1。