【问题标题】:Why Does This Improve Performance?为什么这会提高性能?
【发布时间】:2010-12-09 06:49:21
【问题描述】:

我有两个 for 循环,它们基本上在两个不同的数组中查找(每个数组的峰值大小约为 2-4k),并根据这些值在第三个数组中设置一个值。出于某种奇怪的原因,这段代码的性能存在两个因素差异,具体取决于我放置两个 for 循环的顺序。

这是第一个设置。它在我的 PC 上执行大约 150 毫秒:

public static int[] SchoolMultiplication(int[] a, int[] b, int numberBase)
{
    List<double> times = new List<double>();
    TimeTest timeTest = new TimeTest();

    int aLen = a.Length;
    int bLen = b.Length;

    int[,] resultMatrix = new int[a.Length + b.Length, aLen];
    int[] result = new int[a.Length + b.Length];

    timeTest.Start();

    for (int horizontalIndex = 0; horizontalIndex < b.Length; horizontalIndex++)
    {
        for (int verticalIndex = 0; verticalIndex < a.Length; verticalIndex++)

        {
            resultMatrix[a.Length + b.Length - 1 - verticalIndex - horizontalIndex, verticalIndex] = a[a.Length - verticalIndex - 1] * b[b.Length - horizontalIndex - 1];
        }
    }

现在如果我只改变这样的循环顺序

for (int verticalIndex = 0; verticalIndex < a.Length; verticalIndex++)
{
    for (int horizontalIndex = 0; horizontalIndex < b.Length; horizontalIndex++)
 {
        resultMatrix[a.Length + b.Length - 1 - verticalIndex - horizontalIndex, verticalIndex] = a[a.Length - verticalIndex - 1] * b[b.Length - horizontalIndex - 1];
    }
}

该方法的总运行时间降至约 400 毫秒。简单的循环顺序交换如何将性能提高近 300%?我想这是某种缓存或指针性能的事情?

【问题讨论】:

  • ab的长度是多少?
  • 答案正是@Mike Daniels 提供的链接中的那个。这是一个非常知名的缓存相关问题/优化示例。
  • 为了获得更好的多维数组性能,您应该考虑使用指针。

标签: c# arrays optimization


【解决方案1】:

这是一个数据整理的事情。将内存视为一维数组。这就是磁盘上的实际排列方式(就计算机而言)。因此,在创建多维数组时,当您更改循环顺序时,您会更改数组的遍历方式。您不是按顺序阅读,而是从一个位置跳到另一个位置。


多维数组在你看来是这样的:

像这样对计算机。遍历的最佳方式具有以下箭头后的索引:

因此,当您更改数组循环时,数组的遍历方式如下:

因此,您会获得更多的缓存未命中和性能较差的算法。

【讨论】:

  • ...这就像电影院中的椅子矩阵...通过逐行遍历来访问每把椅子比逐列更快...
  • 但是,如果没有缓存,遍历随机存取存储器 (RAM) 的顺序并不重要(假设所有数组都在 RAM 上)-“因此,随机一词指的是任何一条数据可以在一个固定的时间内返回,无论它的物理位置如何,也不管它是否与前一条数据相关。[1]" en.wikipedia.org/wiki/Random-access_memory
【解决方案2】:

数据的局部性、局部性、局部性。来自维基百科(它比我说的更好):

线性数据结构:局部性经常发生,因为代码包含倾向于通过索引引用数组或其他数据结构的循环。顺序局部性是空间局部性的一种特殊情况,发生在相关数据元素被线性排列和访问时。例如,从基地址到最高元素的一维数组中元素的简单遍历将利用数组在内存中的顺序局部性。 [2]更一般的等距局部性发生在线性遍历具有相同结构和大小的相邻数据结构的较长区域上时,除此之外,不是整个结构都可以访问,而只是结构中相互对应的相同元素。当矩阵表示为行的顺序矩阵并且要求访问矩阵的单列时就是这种情况。

【讨论】:

    【解决方案3】:

    这很可能与缓存命中/未命中有关。区别在于顺序访问与分散访问,后者的大小大于一个缓存行的大小。

    对于普通的 c++ 循环,它还有助于使循环倒退以获得一些循环性能。不确定它如何适合 .NET。

    【讨论】:

    • 为什么让循环倒退会有帮助?
    • 如果您查看汇编代码,测试会更容易。当循环到 0 时,测试很容易,因为您减少并测试 CPU 的 Z 标志。通过比较另一个限制,您必须添加额外的 CMP(以 X86 CPU 为例)
    【解决方案4】:

    你的直觉是对的,这是一个缓存问题。 @Mike Daniels 在下面发布的问题基本上描述了完全相同的问题。第二段代码将获得更多的缓存命中。

    Fastest way to loop through a 2d array?

    但是,嘘,我们不应该关心性能,对吧? :)

    【讨论】:

    • 这段代码是为 C# 的性能竞赛而编写的,所以它绝对是至关重要的。不敢相信我没有想到内存存储。
    • @Qua,是的,我只是在开玩笑。许多人目前的党派路线似乎是表现不再重要。但这很愚蠢。
    【解决方案5】:

    我也认为数组 a 和 b 的相对大小会有所不同。

    如果 a.length 大而 b.length 小,则第二个选项应该更快。 相反,如果 a.length 小而 b.length 大,则第一个选项会更快。 问题在于避免内部循环的设置/拆卸成本。

    顺便说一句,你为什么有

    int aLen = a.Length;

    但那也直接调用a.Length?看来你应该选择其中之一。

    【讨论】:

    • 在分析代码试图弄清楚发生了什么时,我尝试缓存数组长度,但您看到的是该尝试的零散部分。没有优化收益,所以我最终摆脱了它。
    • 为什么如果 a.length 大 b.length 小,第二个选项应该更快?
    【解决方案6】:

    我记得在Code Complete 中读到过这个。在大多数语言中,数组是按顺序设置的最后一个索引设置的,因此您在遍历最后一个索引时直接连续访问字节,而不是在遍历第一个索引时跳过。

    【讨论】:

    • 最后一个索引是数据将按顺序排列的索引,而不是第一个。
    猜你喜欢
    • 1970-01-01
    • 2012-10-03
    • 2017-11-19
    • 2011-04-27
    • 1970-01-01
    • 1970-01-01
    • 2015-05-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多