【问题标题】:Performance issues with algorithm for calculating the average of an array in .NET.NET 中计算数组平均值的算法的性能问题
【发布时间】:2016-01-23 08:54:07
【问题描述】:

以下代码创建一个平均数组,包含 200,000 个数组,每个数组包含 512 个元素。

void Main()
{
    double[] avg = new double[512];
    int start = System.Environment.TickCount;

    for (int i = 0; i < 200000; i++)
    {
        for (int j = 0; j < avg.Length; j++)
        {
            // The `i` in `i-avg[j]` is a dummy for the measured variable.
            avg[j] = avg[j] + (i - avg[j])/(i + 1);
        }
    }

    Console.WriteLine(System.Environment.TickCount-start);
}

迭代平均的原因是为了避免200,000个数组的值求和时溢出。

在现实世界中,200,000 个数组是使用 FFTW 库在 250 毫秒内生成的 DFT。我有点惊讶的是,在我的系统上计算平均数组大约需要 500-600 毫秒(平均),即迭代和几次 flops 需要的时间是 FFT 的 2-3 倍。

有没有办法在 .NET 中使用不同(更快)的方式来加快速度或达到相同的结果,还是我必须切换语言以提高速度?

【问题讨论】:

  • 您可以将我键入的循环计数器声明为双精度。每次我们在内部执行操作 .NET 时都会进行类型转换。我们可以节省此操作的成本。
  • 不确定,但试试 Parallel.For 看看是否有帮助。
  • 如果你去掉 TickCount,它会快一倍。
  • 除法非常慢,比加法慢 10-30 倍。也许您可以对 10 个数字的批次使用快速平均,而对这些批次仅使用较慢的迭代平均公式。

标签: c# algorithm performance


【解决方案1】:

您可以通过将内部循环展开除数 512 来节省一点点。另外我不确定我是否理解 i 循环的用途,因为它代表 (i+1) 可以预先计算并用乘法代替。有没有办法在您的实际案例中应用相同的优化,或者您这样做只是为了提高计时的准确性?

        for (double i = 0; i < 200000; i++)
        {
            var inv_i_plus_1 = 1.0 / (i + 1);
            for (int j = 0; j < avg.Length; )
            {
                // The `i` in `i-avg[j]` is a dummy for the measured variable.
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
                avg[j] = avg[j] + (i - avg[j]) * inv_i_plus_1; j++;
            }
        }

【讨论】:

  • i 循环遍历 200,000 个数组中的每一个,j 循环遍历 i-th 数组的每个值。您的优化建议适用于现实世界。
  • 我还想看看为启用 C# 的 SIMD 扩展所做的工作,即 MSDN documentationsamplesDr Dobbs article 和许多其他 web tutorials。这些有可能将您的表现提高 4 倍!
  • @kasperhj 我已经从内部的for 语句中删除了j++,所以最后一个是绝对需要的。如果您想指出您认为其他人代码中的错误,请添加评论以便首先对其进行审查,不要自己去编辑。
  • 对不起。同意。我已经 c/p'ed 原始循环。
【解决方案2】:

表达式

avg[j] + (i - avg[j]) / (i + 1)

化简后变成

(avg[j] + 1) * i / (i + 1)

如我们所见,i / (i + 1)j 没有任何共同之处,因此我们可以在内循环之外对其进行预先计算

for (int i = 0; i < 200000; i++)
{
    double k = i;
    k = k / (k + 1);
    for (int j = 0; j < avg.Length; j++)
    {
        avg[j] = (avg[j] + 1) * k;
    }
}

最终的代码比原始代码快了大约 4 到 6 倍。

【讨论】:

  • 嗯,i-avg[j] 在现实世界中类似于dft[i]-avg[j],我试图在代码注释中说明这一点。所以我认为第一个简化不会成立。
  • 那么运气不好:(如果dft[i]真的使用i而不是j,仍然可以进行一些(不同的)简化。你为什么不显示确切的情况。
  • 只是为了编译一些代码。它可能应该更具体一点。
  • 没关系。我做了一些额外的测试,关键是用预先计算的乘法因子替换除法(就像在@Mark Feldman 的回答中一样),只是这样做的时间从 0.46s 到 ~0.01s。所以功劳归马克。展开内部循环似乎并没有带来明显的性能提升。
【解决方案3】:

主要原因一定是内循环的划分。

您可能会牺牲一点准确性并进行直接求和。双打没有溢出的风险。

如果值按升序或降序(或接近)排序,则按升序求和将保持良好的准确性。

【讨论】:

    【解决方案4】:

    只需将除法更改为倒数乘法,然后将倒数计算提升到循环之外:

    void Main()
    {
        double[] avg = new double[512];
        int start = System.Environment.TickCount;
    
        for (int i = 0; i < 200000; i++)
        {
            double di = (double)i;
            double r = 1/(di + 1);
    
            for (int j = 0; j < avg.Length; j++)
            {
                // The `i` in `i-avg[j]` is a dummy for the measured variable.
                avg[j] = avg[j] + (di - avg[j]) * r;
            }
        }
    
        Console.WriteLine(System.Environment.TickCount-start);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-14
      • 2013-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多