ILNumerics 在向量上的简单数学运算与系统数组和 Math.NET 的性能对比

dor*_*mon 3 c# numerical ilnumerics math.net

我最近正在研究 C# 中的数值算法。因此我做了一些实验来寻找最适合.NET 的数学库。我经常做的一件事是评估目标函数,这些函数通常是将向量作为输入并返回向量作为输出的函数。我比较了 ILNumerics、系统数组和 Math.NET 中相同目标函数的实现。ILNumerics 的语法确实使其脱颖而出,因为它类似于 MatLab 和 R 的冗长数学公式。然而,我发现对于相同数量的评估,ILNumerics 似乎比 Math.NET 的任何一个系统数组花费的时间都要长得多。下面是我用来比较的代码。我在这里不做任何线性代数,只是纯粹将数学公式应用于长向量。

[Test]
public void TestFunctionEval()
{
    int numObj = 2;
    int m = 100000;
    Func<double[], double[]> fun1 = (x) =>
    {
        double[] z = new double[numObj];
        z[0] = x[0];
        double g = 1.0;
        for (int i = 1; i < x.Length; i++)
            g = g + 9.0 * x[i] / (m - 1);
        double h = 1.0 - Math.Sqrt(z[0] / g);
        z[1] = g * h;
        return z;
    };

    Func<ILArray<double>, ILArray<double>> fun2 = (x) =>
    {
        ILArray<double> z = zeros(numObj);
        z[0] = x[0];
        ILArray<double> g = 1.0 + 9.0 * sum(x[r(1, end)]) / (m - 1);
        ILArray<double> h = 1.0 - sqrt(z[0] / g);
        z[1] = g * h;
        return z;
    };

    Func<Vector<double>, Vector<double>> fun3 = (x) =>
    {
        DenseVector z = DenseVector.Create(numObj, (i) => 0);
        z[0] = x[0];
        double g = 1.0 + 9.0*(x.SubVector(1, x.Count - 1) / (m - 1)).Sum();
        double h = 1.0 - Math.Sqrt(z[0] / g);
        z[1] = g * h;
        return z;
    };

    int n = 1000;
    ILArray<double> xs = rand(n, m);
    IList<double[]> xRaw = new List<double[]>();
    for (int i = 0; i < n; i++)
    {
        double[] row = xs[i, full].ToArray();
        xRaw.Add(row);
    }
    DenseMatrix xDen = DenseMatrix.OfRows(n, m, xRaw);
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < n; i++)
    {
        ILArray<double> ret = fun1(xRaw[i]);
    }
    watch.Stop();
    log.InfoFormat("System array took {0} seconds.", watch.Elapsed.TotalSeconds);
    watch.Reset();
    watch.Start();
    for (int i = 0; i < n; i++)
    {
        ILArray<double> ret = fun2(xs[i, full]);
    }
    watch.Stop();
    log.InfoFormat("ILNumerics took {0} seconds.", watch.Elapsed.TotalSeconds);
    watch.Reset();
    watch.Start();
    for (int i = 0; i < n; i++)
    {
        var ret = fun3(xDen.Row(i));
    }
    watch.Stop();
    log.InfoFormat("Math.Net took {0} seconds.", watch.Elapsed.TotalSeconds);
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,测试表明 ILNumerics 做如此简单的事情花费的时间太长。

315 | System array took 0.7117623 seconds.
323 | ILNumerics took 14.5100766 seconds.
330 | Math.Net took 5.3917536 seconds.
Run Code Online (Sandbox Code Playgroud)

我真的很喜欢它使代码看起来非常像数学公式的方式。然而,花费比系统数组或 Math.NET 多倍的时间来评估上述函数意味着我必须选择其他替代方案而不是 ILNumerics,即使这会导致函数的解释时间更长且更困难。

我是否以错误的方式使用 ILNumerics?或者在这种情况下它的设计速度会变慢。也许我没有将它用于最合适的目的。有人可以解释一下吗?

测试中使用ILNumerics 3.2.2.0和Math.NET.Numerics 2.6.1.30。

Hay*_*ach 5

是的,您缺少一些通用的性能测试规则。而且这种比较也不公平:

  1. 对于 ILNumerics 实现,您创建了许多相当大的临时变量。与其他实现相比,这是不利的,在其他实现中,您仅创建一次长向量并在“内部循环”中执行所有操作。内部循环总是会更快——但代价是语法表达能力较差,编程工作量更大。如果您需要这种性能,您始终可以使用 x.GetArraysForRead() 和 x.GetArrayForWrite() 直接使用底层 System.Array。这可以让您从 System.Array 测试中获得选项...

  2. 您在 ILNumerics 测试中包含了大量子数组创建(以及新的内存分配),而其他测试中未包含这些子数组。例如,您可以从测量循环内的大测试数据矩阵导出子数组。

为什么不这样设计测试:为每个测试单独创建 1 个大矩阵。使用 Mathnet 矩阵进行 Mathnet 测试,使用 System.Array 矩阵进行 System.Array 测试,使用 ILArray 进行 ILNumerics。在每次迭代中,提取相应的行并将其交给相应的函数。

不要忘记遵循 ILNumerics 函数规则:http://ilnumerics.net/GeneralRules.html并使用不附加任何调试器的发布版本运行测试。像往常一样,忽略第一次迭代所需的时间。

根据您的系统(及其带来的自动并行化选项),ILNumerics 可能仍然较慢。在这种情况下,请考虑遵循ILNumerics 的进一步优化选项或通过求助于 System.Array 来优化内部循环。

@编辑:还有一点:您可能知道这样一个事实,即在没有实际做任何有用的事情的情况下进行此类微观测试总是会产生误导。结果可能不适合从中得出对最终应用程序性能的期望。举个例子:如果您长时间仅使用 System.Array 迭代大型数组,您最终可能会将大部分时间花在 GC 上,而不是计算数字。您必须小心,不要重新分配任何存储,这会使您的代码更加笨拙。

ILNumerics - 如果使用正确 - 通过自动重用内存来防止您在 GC 上花费时间。此外,它还在内部并行化您的算法(即使仅使用向量对于并行化要求不够,如您的示例所示)。