【问题标题】:Why is it worse to initialize a two dimensional array like this?为什么像这样初始化二维数组会更糟​​?
【发布时间】:2012-06-24 07:15:40
【问题描述】:
for(int i = 0; i<100; i++)

    for(int j = 0; j<100; j++)

         array[j][i] = 0;
         // array[i][j] = 0;

我的教授说以第一种方式初始化二维数组的成本要高于第二种方式。有人可以解释导致这种情况的引擎盖下发生了什么吗?或者,这两种初始化方式的性能是否相同?

【问题讨论】:

  • 参考位置:您以“慢”的方式不必要地使 CPU 缓存失效。
  • @dlev:你为什么不把这个作为答案发布?
  • 因为 dlev 与代表无关。 dlev 是关于爱的
  • 这份有用的文档非常详细地描述了内存局部性和许多其他事实:akkadia.org/drepper/cpumemory.pdf
  • 您的教授可能在谈论数组的访问模式,而不是的初始化。初始化(在声明时)有自己的语法(double array[100][100] = { 0 };),现代编译器中的实现可能“优于”这里所说的任何东西。

标签: c arrays assembly multidimensional-array localityofreference


【解决方案1】:

正如@dlev 提到的,这是由于locality of reference 造成的,并且与计算机中物理硬件的工作方式有关。

在计算机内部,有许多不同类型的内存。通常,只有某些内存位置(寄存器)才能对其执行实际操作;其余时间,如果您对数据执行操作,则必须将其从内存加载到寄存器中,执行一些计算,然后将其写回。

主存储器 (RAM) 比寄存器慢得多,通常要慢数百到数千倍。因此,应尽可能避免从内存中读取。为了解决这个问题,大多数计算机通常具有称为caches 的特殊内存区域。缓存的工作是保存最近从内存中访问过的数据,这样如果再次访问相同的内存区域,则可以从缓存中(快速)而不是从主内存(慢)中提取值。通常,缓存的设计是这样的,如果从内存中读取一个值,则该值加上一大堆相邻的值被拉入缓存。这样,如果你遍历一个数组,那么在读取第一个值之后,数组中的其余值将位于缓存中,并且可以更有效地访问。

您的代码比它需要的慢的原因是它没有按顺序访问数组元素。在C语言中,二维数组布局在row-major order,也就是说内存是这样排列的

A[0][0] A[0][4] A[0][5] ... A[1][0] A[1][6] A[1][7] ... A[2][0] A[2][8] A[2][9] ...

因此,如果你使用这个 for 循环:

for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        // Do something with A[i][j]
    }
}

然后您将获得极好的局部性,因为您将按照数组元素在内存中出现的顺序访问它们。这使得主内存的读取次数非常少,因为所有内容通常都在缓存中并准备就绪。

但是,如果您交换循环,就像您所做的那样,您的访问会在内存中跳转并且不一定是连续的。这意味着您将有很多缓存未命中,其中您接下来读取的内存地址不在缓存中。这会增加缓存加载的数量,从而大大降低程序的速度。

编译器开始变得足够聪明,可以自动交换这样的循环,但我们距离忽略这些细节还有很长的路要走。作为一般规则,在为多维数组编写 C 或 C++ 代码时,请尝试以行优先顺序而不是列优先顺序进行迭代。您可以在程序中获得明显的加速。

希望这会有所帮助!

【讨论】:

  • 你希望我相信这是在 8 分钟内完成的吗?噗。 (一个非常好的答案。)
  • @pst- 我每年夏天都会教一门编译器课程,现在正在复习我的幻灯片,所以所有这些都在我的记忆中记忆犹新。 (我刚刚意识到这意味着我可以快速输入它,因为它在缓存中......令人毛骨悚然......)
  • 这里可能不适用,但你也可以稍微展开循环,或者让编译器为你做这件事,以获得一些潜在的加速。这也可以让缓存和管道有一些优势。当然,您也可能对正在运行的硬件有一些深入的了解。
  • 我似乎记得在“The Mythical Man-Month”(一流的书。每个人都读过,对吗?)中,Fred Brooks 回顾了在 IBM 完成的关于 locality-of 的工作-reference 及其对虚拟内存潜在实用性的影响。 (我还记得 RADM Grace Murray Hopper 说过,当她发明它时,她称其为“使用辅助存储”。霍尼韦尔开发它时,他们称其为“使用辅助存储”。但是,她抱怨说,它只是在 IBM 接管时才被广泛采用称之为“虚拟内存”)。
【解决方案2】:

我可能会因此而被否决,但如果您正在编程 C,那么“最好的”很可能是:

memset(array, 0, sizeof(array));

然后,您可以将所有优化(您显然很担心)的责任推给 memset 的实现。任何特定的硬件优势都可以在那里实现。

http://en.wikipedia.org/wiki/Sizeof#Using_sizeof_with_arrays/

http://www.cplusplus.com/reference/clibrary/cstring/memset/

另一个观察是,如果你初始化为零,问问自己为什么?如果您的数组是静态的(对于这么大的可能是哪个?),那么 cstartup 将为您初始化为零。同样,这可能会为您的硬件使用最有效的方式。

【讨论】:

  • +1 - 在 C 中,对标准库函数的调用总是按顺序进行的。
  • 在 c 中使用标准结构比库函数更好:有一种用于初始化数组的语法。
  • @Josh - 我使用的编译器都知道将零分配给数组的循环是初始化。生成的代码与使用 memset(也是“已知”)没有什么不同。
【解决方案3】:

我参加聚会有点晚了,已经有一个很好的答案。但是,我认为我可以通过演示如何使用分析工具(在 Linux 上)通过实验回答这个问题来做出贡献。

我将使用 Ubuntu 10.10 软件包 linux-tools-common 中的 perf 工具。

这是我为回答这个问题而编写的小 C 程序:

// test.c
#define DIM 1024

int main()
{
    int v[DIM][DIM];
    unsigned i, j;

    for (i = 0; i < DIM; i++) {
        for (j = 0; j < DIM; j++) {
#ifdef ROW_MAJOR_ORDER
            v[i][j] = 0;
#else
            v[j][i] = 0;
#endif
        }
    }

    return 0;
}

然后编译两个不同的版本:

$ gcc test.c -O0 -DROW_MAJOR_ORDER -o row-maj
$ gcc test.c -O0 -o row-min

注意我已经禁用了 -O0 的优化,所以 gcc 没有机会重新安排我们的循环以提高效率。

我们可以通过perf list 列出perf 可用的性能统计信息。在这种情况下,我们对缓存未命中感兴趣,即事件cache-misses

现在它就像多次运行程序的每个版本并取平均值一样简单:

$ perf stat -e cache-misses -r 100 ./row-min

 Performance counter stats for './row-min' (100 runs):

             286468  cache-misses               ( +-   0.810% )

        0.016588860  seconds time elapsed   ( +-   0.926% )

$ perf stat -e cache-misses -r 100 ./row-maj

 Performance counter stats for './row-maj' (100 runs):

               9594  cache-misses               ( +-   1.203% )

        0.006791615  seconds time elapsed   ( +-   0.840% )

现在我们已经通过实验验证,您确实看到“row-minor”版本的缓存未命中率实际上增加了两个数量级。

【讨论】:

    【解决方案4】:

    如果您查看每种技术访问的内存位置,第二个将访问连续的字节,而第一个将跳跃 100 个字节。如果采用第二种方式,内存缓存的工作效率会更高。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-10-17
      • 2015-02-09
      • 1970-01-01
      • 2021-09-21
      • 2012-11-29
      相关资源
      最近更新 更多