循环通过二维数组的最快方法?

时间:2009-06-15 17:02:42

标签: optimization caching loops

我偶然发现this blog post。作者展示了两个循环通过矩形并计算某些东西的代码示例(我的猜测是计算代码只是一个占位符)。在其中一个示例中,他垂直扫描矩形,另一个水平扫描矩形。然后他说第二个是最快的,每个程序员都应该知道原因。现在我不能成为程序员,因为对我来说它看起来完全一样。任何人都可以向我解释一个吗?

感谢。

6 个答案:

答案 0 :(得分:54)

缓存一致性。当您水平扫描时,您的数据将在内存中更加紧密,因此您将减少缓存未命中率,从而提高性能。对于一个足够小的矩形,这没关系。

答案 1 :(得分:7)

答案已被接受,但我不认为这是整个故事。

是的,缓存是所有这些元素必须以某些顺序存储在内存中的一个重要原因。如果按照存储顺序对它们进行索引,则可能会减少缓存未命中数。可能的。

另一个问题(许多答案也提到)是几乎每个处理器都有一个非常快速的整数增量指令。它们通常没有非常快的“增加一定量乘以第二个任意数量”。当你用“反对谷物”作为索引时,这就是你所要求的。

第三个问题是优化。已经对优化这种循环进行了大量的努力和研究,如果你以一种合理的顺序对它进行索引,你的编译器将更有可能将这些优化之一付诸实施。

答案 2 :(得分:5)

缓存确实是原因,但是如果你想知道论证的内容,你可以看看U. Drepper的“每位程序员应该知道的内容”:

http://people.redhat.com/drepper/cpumemory.pdf

答案 3 :(得分:4)

稍微扩展先前的答案:

通常,作为程序员,我们可以将程序的可寻址内存视为一个平坦的字节数组,从0x00000000到0xFFFFFFFF。操作系统将保留其中一些地址(比如说低于0x800000000的所有地址)供自己使用,但我们可以用其他地址做我们喜欢的事情。所有这些内存位置都存在于计算机的RAM中,当我们想要从中读取或写入它们时,我们会发出适当的指令。

但事实并非如此!有许多并发症污染了这个简单的进程内存模型:虚拟内存,交换和缓存

与RAM交谈需要相当长的时间。它比转到硬盘要快得多,因为没有任何旋转盘或磁铁,但现代CPU的标准仍然相当慢。因此,当您尝试从内存中的特定位置读取时,您的CPU不只是将该位置读入寄存器并将其称为良好。相反,它会将该位置和/或一堆附近的位置读取到位于CPU上的处理器缓存中,并且可以比主存更快地访问。

现在我们对计算机的行为有一个更复杂但更正确的看法。当我们尝试读取内存中的位置时,首先我们查看处理器缓存以查看该位置的值是否已存储在那里。如果是,我们使用缓存中的值。如果不是这样,我们需要更长时间进入主内存,检索该值以及它的几个邻居,并将它们粘贴在缓存中,踢出一些曾经用于腾出空间的东西。

现在我们可以看到为什么第二个代码片段比第一个代码片段快。在第二个示例中,我们首先访问a[0]b[0]c[0]。这些值中的每一个都与其邻居一起缓存,例如a[1..7]b[1..7]c[1..7]。然后,当我们访问a[1]b[1]c[1]时,它们已经在缓存中,我们可以快速阅读它们。最终我们到达a[8],并且必须再次进入RAM,但是八次中有七次我们正在使用漂亮的快速缓存而不是笨重的慢速RAM内存。

(那么为什么不访问abc从缓存中互相踢出来?这有点复杂,但基本上处理器决定在哪里存储缓存中的给定值由其地址组成,因此在空间上彼此不相邻的三个对象不太可能被缓存到同一位置。)

相比之下,考虑一下lbrandy帖子的第一个片段。我们首先阅读a[0]b[0]c[0],缓存a[1..7]b[1..7]c[1..7]。然后,我们访问a[width]b[width]c[width]。假设宽度为> = 8(它可能是,或者我们不关心这种低级优化),我们必须再次进入RAM,缓存一组新的值。当我们到达a[1]时,它可能已被踢出缓存以为其他东西腾出空间。在三个大于处理器缓存的阵列的情况下,很可能每个读取/都会丢失缓存,从而极大地降低了性能。

这是对现代缓存行为的高级别讨论。对于更深入和技术性的内容,this看起来像是对主题的彻底但可读的处理。

答案 4 :(得分:1)

是的,'缓存一致性'......当然,这取决于你可以优化垂直扫描的内存分配。传统上,视频内存从左到右,从上到下进行分配,然后返回。我确信CRT屏幕以相同的方式绘制扫描线的日子。理论上你可以改变这一点 - 所有这一切都说明横向方法没有任何内在的东西。

答案 5 :(得分:-1)

原因是当你进入内存布局的硬件级别时,实际上没有二维数组这样的东西。因此,“垂直”扫描以进入您需要访问的下一个单元格,您正在沿着这些行进行操作

对于索引为(行,列)的2D数组,需要将其转换为数组[index]的单维数组,因为计算机中的内存是线性的。

因此,如果您正在垂直扫描,则下一个索引计算为:

index = row * numColumns + col;

但是,如果您正在水平扫描,那么下一个索引如下:

index = index++;

单个添加将减少CPU的操作码,然后是乘法和加法,因此由于计算机内存的架构,水平扫描速度更快。

缓存不是答案,因为如果这是您第一次加载此数据,则每次数据访问都将是缓存未命中。对于第一次执行,水平更快,因为操作更少。通过三角形的后续循环将通过缓存更快,并且如果三角形足够大,则垂直可能更慢,因为缓存未命中,但由于操作数量增加,总是比水平扫描慢需要访问下一个元素。