【问题标题】:Why is iterating through the outer dimension with an outer loop faster than with an inner loop?为什么使用外循环比使用内循环更快地遍历外部维度?
【发布时间】:2017-08-23 22:32:38
【问题描述】:

让我们考虑一个矩阵

std::vector<std::vector<int>> matrix;

每一行的长度相同。我将每个std::vector&lt;int&gt; 称为一个列。

为什么使用外循环比使用内循环更快地遍历外部维度?

第一个程序:首先遍历列

int sum = 0;
for (int col = 0 ; col < matrix.size() ; col++)
{
   for (int row = 0 ; row < matrix[0].size() ; row++)
   {
      sum += matrix[col][row];
   }
}

第二个程序:首先遍历行

int sum = 0;
for (int row = 0 ; row < matrix[0].size() ; row++) // Assuming there is at least one element in matrix
{
   for (int col = 0 ; col < matrix.size() ; col++)
   {
      sum += matrix[col][row];
   }
}

这是我的猜测

在记忆中跳跃

我可能有一种模糊的直觉,即在内存中跳转会比读取连续的内存花费更多的时间,但我认为 RAM 的内存访问需要恒定的时间。另外,DRAM 中没有移动部件,我不明白为什么读取两个连续的ints 会更快?

巴士宽度

int 占用 2 个字节(尽管可能因数据模型而异)。在具有 8 字节宽总线的机器中,我可以想象最终如果 ints 在内存中是连续的,那么每个时钟周期可以将 4 个 ints(取决于数据模型)发送到处理器,如果int 不连续,则每个时钟周期只能发送一个。

如果是这种情况,那么如果 matrix 将包含 8 字节长的 long long int,我们将不会再看到这两个程序之间有任何区别(我还没有测试过)。

缓存

我不知道为什么,但我觉得缓存可能是第二个程序变慢的原因。缓存的影响可能与我刚才谈到的总线大小参数有关。有可能只有 DRAM 中连续的内存才能加载到缓存中,但我不知道为什么会这样。

【问题讨论】:

    标签: performance caching memory ram


    【解决方案1】:

    是的,是cache

    有一个奇怪的巧合1,当程序访问内存中的数据时,它们通常会立即或不久之后访问附近的数据。

    CPU 设计人员意识到了这一点,因此设计缓存来一次加载整个内存块。

    因此,当您访问 matrix[0][0] 时,即使不是所有 matrix[0] 的其余部分都与 matrix[0][0] 的单个元素一起被拉入缓存,但很有可能 matrix[20] 没有任何东西成功进入缓存。

    请注意,这取决于由连续数组组成的矩阵,至少在最后一维是这样。如果您使用的是链接列表,您可能2 不会看到太大的差异,相反,无论访问顺序如何,都会体验到较慢的性能。

    原因是缓存加载了连续的块。考虑matrix[0][0] 是否指的是内存地址0x12340000。访问该字节将加载该字节,再加上接下来的 127 个字节到缓存中(确切数量取决于 cpu)。因此,您将拥有从 0x123400000x1234007F 的每个字节在缓存中。

    在一个连续数组中,0x12340004 处的下一个元素已经在缓存中。但是链表不是连续的,下一个元素几乎可以在任何地方。如果它超出了0x123400000x1234007F 的范围,那么你什么也得不到。


    1 仔细想想,这真的不是那么奇怪的巧合。使用本地堆栈变量?访问相同的内存区域。遍历一维数组?对同一内存区域的大量访问。遍历二维数组,外部循环中的外部维度和内部嵌套循环中的内部数组?基本上是遍历一堆一维数组。

    2 你可能会走运并且让你的链表的节点彼此相邻,但这似乎是一个非常不可能的情况。由于指向下一个元素的指针会占用空间,因此您仍然无法在缓存中容纳那么多元素,并且间接会对性能造成额外的小影响。

    【讨论】:

    • 计算机科学家有时甚至会根据这个因素分析算法,他们称之为 I/O 效率(或有时是缓存效率)。
    • @8bittree 谢谢!我不确定我是否理解为什么非连续内存不会像连续内存那样加载到缓存中?
    • 这实际上取决于。有时数组以连续的行存储,而另一些时间以连续的列存储,因此在行间循环可能并不总是更快
    • @Alex 当然我明白我在帖子中所说的行和列是任意的。
    • @Remi.b 太好了。只是为了确保每个人都能得到这个细节:)
    【解决方案2】:

    当 Going Column - row 时,您会像这样计数 ([C][R]) [0][0] + [0][1] + [0][2] ... 等等。所以你不是在数组的元素之间切换。

    当进行行 - 列时,您会像这样计数 ([C][R]) [0][0] + [1][0] + [2][0] 这样您就可以在元素之间切换每次阵列,因此在 DRAM 中需要更长的时间。

    二维数组的处理方式如下: new Array{array1, array2, array3};数组内的数组。倒数数组 (C-R) 比切换数组并计算同一行中的元素 (R-C) 更快。

    数组是一块分割的内存,所以当你有二维数组并计算 (R-C) 时,你会在速度较慢的 DRAM 中跳来跳去。

    DRAM中没有机械部件没关系,跳来跳去会慢一些。示例:SRAM 没有机械部件,但比 DRAM 慢(当然尺寸更大),因为需要更大尺寸的额外晶体管和电容器。

    edit 阅读其他答案后,我想包括当您迭代 (C-R) 时,可以将整个元素加载到缓存中以便快速访问。但是当每次(R-C)将新的数组元素加载到缓存中时效率不高,或者由于效率低下而可能不会发生。

    【讨论】:

    • 由于声誉问题,我无法对问题发表评论。所以在这里我想说这是一个很好的问题,但是你有没有做过测试来检查这个问题?结果如何?我想知道它慢了多少。
    • 这与RAM的含义完全相反:随机存取内存。每个地方都有相同的访问时间。正如另一个答案所说,它与缓存有关,特别是,如果 cpu 正在缓存行或列。
    • @Alex DRAM 由于内存层次结构而具有可变的访问时间,这与它的名字是违反直觉的。
    猜你喜欢
    • 1970-01-01
    • 2016-10-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多