为什么使用外层循环迭代外部维度比内层循环更快?

3

让我们考虑一个矩阵

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

每行长度相同,我将每个std :: vector<int>称为一列。

为什么用外循环迭代外部维度比用内循环更快?

第一个程序:首先迭代列

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中没有移动部件,我不明白如果两个int是连续的,为什么会更快地读取它们?
总线宽度
一个int占用2个字节(尽管根据数据模型可能会有所变化)。在一台具有8字节宽总线的机器上,我可以想象,如果int在内存中是连续的,那么每个时钟周期最终可能会将4个int(根据数据模型)发送到处理器,而如果它们不是连续的,则每个时钟周期只能发送一个int。
如果是这样的话,那么如果矩阵包含长度为8字节的long long int,则我们将不再看到两个程序之间的任何区别(我还没有测试过)。
缓存
我不确定为什么,但我感觉缓存可能是第二个程序速度较慢的原因。缓存的效果可能与我上面提到的总线大小参数有关。可能只有在DRAM中连续的内存才能加载到缓存中,但我不知道为什么会这样。
2个回答

4

没错,它是缓存

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

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

因此,当您访问matrix[0][0]时,除了在matrix[0][0]处的单个元素之外,matrix[0]的大部分,如果不是全部,也被拉入了缓存中,而从matrix[20]中拉入缓存的可能性很小。

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

原因是缓存加载连续块。考虑如果matrix [0] [0]引用内存地址0x12340000。访问它将加载该字节以及下一个127个字节到缓存中(确切数量取决于CPU)。因此,您将在缓存中拥有从0x123400000x1234007F的每个字节。
在连续数组中,位于0x12340004的下一个元素已经在缓存中。但是链表不是连续的,下一个元素可以是任何位置。如果它在0x123400000x1234007F范围之外,则您没有获得任何好处。

1 如果你仔细想想,这并不是一个奇怪的巧合。使用本地堆栈变量?访问同一内存区域。遍历一维数组?访问同一内存区域很多次。在外循环中遍历二维数组,内部数组在内部嵌套循环中?基本上是在遍历一堆一维数组。

2 你可能会运气好,链表节点都挨在一起,但这似乎是一个非常不可能的情况。由于下一个元素的指针占用空间,而且还会有额外的小性能损失,因此你仍然无法将尽可能多的元素放入缓存中。


@Alex 确定,我理解我在帖子中所称的行和列是任意的。 - Remi.b
@Remi.b 很好。只是为了确保每个人都理解这个细节 :) - Alex
@Remi.b 这背后的主要原因是因为对于硬盘(也适用于固态硬盘),前往任意位置(随机访问)的操作是一项非常昂贵的操作。它需要非常长的时间(与其他操作相比)。然而,一旦到达该位置,读取周围的下一组字节则是非常快速的。因此,处理器决定,在已经到达该位置时,直接加载一堆其他周围的字节,因为在许多应用程序中,它们将在不久的将来变得相关。 - Zabuzard
@Remi.b 给你一个大致的尺寸概述:HDD 的搜索时间约为 5 毫秒,SSD 约为 0.1 毫秒,而传输速率(读取字节)约为 HDD 的 50 MB/s(每个字节大约 20ns),SSD 的传输速率约为 200 MB/s。 - Zabuzard
@Remi.b 我添加了几段来扩展为什么连续性很重要。这有帮助吗? - 8bittree
显示剩余3条评论

-1

当按列-行进行计数时,你是这样计算的([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)更快。

数组是内存的一个分段,所以当你有二维数组并且按行-列计数时,你会在DRAM中跳来跳去,这会更慢。

DRAM中没有机械部件并不重要,跳来跳去仍然会更慢。例如:SRAM没有机械部件,但比DRAM(当然是更大尺寸的)更慢,因为由于额外晶体管和电容器的尺寸更大,需要行进的距离更长。

编辑 在阅读了其他答案之后,我想指出在迭代(C-R)时整个元素可以加载到缓存中以快速访问。但是在进行(R-C)时,每次加载一个新的数组元素到缓存中都不太有效或者由于效率低下而不会发生。


由于声望不够,我无法对这个问题进行评论。所以在这里我想说这是一个非常好的问题,但你是否已经进行了测试来验证它?结果如何?我想知道它慢了多少。 - DR. Palson_PH.d
这正好与RAM的意义相反:随机存取内存。每个位置的访问时间都相同。正如其他答案所说,这与缓存有关,具体来说,与CPU是否缓存行或列有关。 - Alex
@Alex DRAM由于存在内存层次结构,具有可变的访问时间,这与其名称相悖。 - DR. Palson_PH.d

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接