【问题标题】:Confusion about different running times of two algorithms in C [duplicate]关于C中两种算法的不同运行时间的混淆[重复]
【发布时间】:2019-02-21 19:18:23
【问题描述】:

我有一个数组long matrix[8*1024][8*1024],还有两个函数sum1sum2

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.19s

sum2:1.25s

谁能解释为什么会有这么大的差异?

【问题讨论】:

  • 你编译时启用了所有零售优化,对吧?
  • @selbie 我用 gcc -0O -lrt matrix_sum.c -o matrix_sum
  • 顺便说一句,register 关键字现在没有什么作用。除了 register-qualified 变量不可寻址这一事实之外,编译器几乎忽略了它。
  • 如果您的计时程序一遍又一遍地执行相同的功能,那么您就是在计时硬件将矩阵移动到缓存中的效率。如果你的缓存足够大,那么整个矩阵在第一次通过后就可以放入缓存中,并且时间差异将很小。
  • 我想知道循环交换优化是否会以同样快的速度呈现两种实现,如 Why is it faster to process a sorted array than an unsorted array? 中所见。

标签: c


【解决方案1】:

C 使用 row-major ordering 来存储多维数组,如 C 标准的 § 6.5.2.1 Array subscripting, paragraph 3 中所述:

连续的下标运算符指定多维数组对象的一个​​元素。如果 E 是维度为 i x j x 的 n 维数组 (n >= 2)。 . . x k,然后 E(用作左值以外的值)被转换为指向具有维度 j x 的 (n - 1) 维数组的指针。 . . xk。如果一元 * 运算符显式地或作为下标的结果隐式地应用于此指针,则结果是引用的 (n - 1) 维数组,如果用作左值以外的数组,它本身将转换为指针。 由此得出,数组以行优先顺序存储(最后一个下标变化最快)。

强调我的。

这是来自Wikipedia 的图片,它展示了这种存储技术与存储多维数组的其他方法的对比,列主要排序

第一个函数sum1 根据二维数组在内存中的实际表示方式连续访问数据,因此数组中的数据已经在缓存中。 sum2 需要在每次迭代时获取另一行,这不太可能在缓存中。

还有一些其他语言对多维数组使用列优先排序;其中包括 R、FORTRAN 和 MATLAB。如果您使用这些语言编写等效代码,您会发现sum2 的输出速度更快。

【讨论】:

  • 赞成实际引用标准。
【解决方案2】:

计算机通常使用 缓存 来帮助加快对主内存的访问。

通常用于主存的硬件相对较慢——数据从主存传送到处理器可能需要很多处理器周期。因此,计算机通常包含少量非常快速但昂贵的内存,称为缓存。计算机可能有多个级别的缓存,其中一些内置在处理器或处理器芯片本身中,而另一些则位于处理器芯片之外。

由于缓存较小,它无法将所有内容都保存在主内存中。它通常甚至无法容纳一个程序正在使用的所有内容。所以处理器必须决定缓存中保存的内容。

程序最频繁的访问是对内存中的连续位置。很多时候,程序读取数组的第 237 个元素后,很快就会读取到 238,然后是 239,以此类推。在读取 237 之后读取 7024 的情况较少。

因此缓存的操作旨在将主内存中连续的部分保留在缓存中。您的sum1 程序可以很好地处理这个问题,因为它可以最快速地更改列索引,在处理所有列的同时保持行索引不变。它访问的数组元素在内存中是连续布局的。

您的sum2 程序不能很好地处理这个问题,因为它会最快速地更改行索引。这会在内存中跳过,因此它进行的许多访问都不能被缓存满足,并且必须来自较慢的主内存。

相关资源:Memory layout of multi-dimensional arrays

【讨论】:

  • 此外,MMU 将在一次操作中将数据行(一系列连续内存地址中的所有内容)提取到缓存中。因此,如果您正在对内存中连续的数据进行操作 - 例如一维数组或二维数组的一维切片 - 那么当数组 [i] 首次访问时,[i+1], [i+2]...[ i+n] 会自动预取到缓存中。
【解决方案3】:

在带有数据缓存的机器上(即使是 68030 也有),在连续内存位置读取/写入数据要快得多,因为一块内存(大小取决于处理器)从内存中提取一次,然后从内存中调用缓存(读取操作)或一次全部写入(写入操作的缓存刷新)。

通过“跳过”数据(远离前一次读取),CPU 必须再次读取内存。

这就是为什么你的第一个 sn-p 更快。

对于更复杂的操作(例如快速傅立叶变换),其中数据被多次读取(与您的示例不同)许多库(例如 FFTW)建议使用 stride 来适应您的数据组织(行/列)。 从不使用它,总是先转置你的数据并使用 1 的步幅,这将比尝试不转置更快。

为确保您的数据是连续的,切勿使用二维符号。首先将您的数据定位在选定的行中并设置指向行首的指针,然后在该行上使用内部循环。

for (i=0; i < ROWS; i++) {
    const long *row = m[i];
    for (j=0; j < COLS; j++) {
        sum += row[j];
    }
}

如果您不能这样做,则意味着您的数据方向错误。

【讨论】:

  • 或者您可以按更有利的顺序使用二维符号。
  • 是的,但我喜欢上下文的概念:选择行,在行上工作。并且内部循环可以移动到另一个使用 SSE 或其他的仅一维 sum/product/whatever 函数。此外,避免非优化编译器每次都计算索引。
【解决方案4】:

这是缓存的问题。

缓存将自动读取位于您请求的数据之后的数据。所以如果你逐行读取数据,你请求的下一个数据已经在缓存中了。

【讨论】:

    【解决方案5】:

    内存中的矩阵是线性对齐的,这样一行中的项目在内存中彼此相邻 (spacial locality)。当您按顺序横向项目以便在移动到下一个之前遍历一行中的所有列时,当 CPU 遇到尚未加载到其缓存中的条目时,它将继续加载该值在物理内存中有一整块接近它的其他值,因此接下来的几个值在需要读取它们时已经被缓存了。

    当您以另一种方式横向移动它们时,它加载的其他在内存中靠近它的值不会成为下一个读取的值,因此您最终会遇到更多的缓存未命中,因此 CPU 必须坐下并等待数据从内存层次结构的下一层引入。

    当您转回之前缓存的另一个条目时,它很可能已从缓存中引导出来,以支持您加载后的所有其他数据,因为它最近不会不再使用 (temporal locality)

    【讨论】:

      【解决方案6】:

      为了扩展其他答案,这是由于第二个程序的缓存未命中,并假设您使用的是 Linux、*BSD 或 MacOS,那么 Cachegrind 可能会给您带来启发。它是 valgrind 的一部分,将运行您的程序,无需更改,并打印缓存使用统计信息。不过它确实运行得很慢。

      http://valgrind.org/docs/manual/cg-manual.html

      【讨论】:

        猜你喜欢
        • 2013-07-05
        • 2011-03-09
        • 1970-01-01
        • 2018-11-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-12-06
        相关资源
        最近更新 更多