为什么使用本地代码可以获得约20%的速度提升?

24

这段代码为什么会出现问题:

extern "C" __declspec(dllexport) void Transform(double x[], double y[], int iterations, bool forward)
{
    long n, i, i1, j, k, i2, l, l1, l2;
    double c1, c2, tx, ty, t1, t2, u1, u2, z;

    /* Calculate the number of points */
    n = (long)pow((double)2, (double)iterations);

    /* Do the bit reversal */
    i2 = n >> 1;
    j = 0;
    for (i = 0; i < n - 1; ++i)
    {
        if (i < j)
        {
            tx = x[i];
            ty = y[i];
            x[i] = x[j];
            y[i] = y[j];
            x[j] = tx;
            y[j] = ty;
        }
        k = i2;
        while (k <= j)
        {
            j -= k;
            k >>= 1;
        }
        j += k;
    }

    /* Compute the FFT */
    c1 = -1.0; 
    c2 = 0.0;
    l2 = 1;
    for (l = 0; l < iterations; ++l)
    {
        l1 = l2;
        l2 <<= 1;
        u1 = 1; 
        u2 = 0;
        for (j = 0; j < l1; j++) 
        {
            for (i = j; i < n; i += l2) 
            {
                i1 = i + l1;
                t1 = u1 * x[i1] - u2 * y[i1];
                t2 = u1 * y[i1] + u2 * x[i1];
                x[i1] = x[i] - t1; 
                y[i1] = y[i] - t2;
                x[i] += t1;
                y[i] += t2;
            }
            z = u1 * c1 - u2 * c2;
            u2 = u1 * c2 + u2 * c1;
            u1 = z;
        }
        c2 = sqrt((1.0 - c1) / 2.0);
        if (forward) 
            c2 = -c2;
        c1 = sqrt((1.0 + c1) / 2.0);
    }

    /* Scaling for forward transform */
    if (forward)
    {
        for (i = 0; i < n; ++i)
        {
            x[i] /= n;
            y[i] /= n;
        }
    }
}

这段代码比这个代码快20%吗?

public static void Transform(DataSet data, Direction direction)
{
    double[] x = data.Real;
    double[] y = data.Imag;
    data.Direction = direction;
    data.ExtremeImag = 0.0;
    data.ExtremeReal = 0.0;
    data.IndexExtremeImag = 0;
    data.IndexExtremeReal = 0;

    long n, i, i1, j, k, i2, l, l1, l2;
    double c1, c2, tx, ty, t1, t2, u1, u2, z;

    /* Calculate the number of points */
    n = (long)Math.Pow(2, data.Iterations);

    /* Do the bit reversal */
    i2 = n >> 1;
    j = 0;
    for (i = 0; i < n - 1; ++i)
    {
        if (i < j)
        {
            tx = x[i];
            ty = y[i];
            x[i] = x[j];
            y[i] = y[j];
            x[j] = tx;
            y[j] = ty;
        }
        k = i2;
        while (k <= j)
        {
            j -= k;
            k >>= 1;
        }
        j += k;
    }

    /* Compute the FFT */
    c1 = -1.0; 
    c2 = 0.0;
    l2 = 1;
    for (l = 0; l < data.Iterations; ++l)
    {
        l1 = l2;
        l2 <<= 1;
        u1 = 1; 
        u2 = 0;
        for (j = 0; j < l1; j++) 
        {
            for (i = j; i < n; i += l2) 
            {
                i1 = i + l1;
                t1 = u1 * x[i1] - u2 * y[i1];
                t2 = u1 * y[i1] + u2 * x[i1];
                x[i1] = x[i] - t1; 
                y[i1] = y[i] - t2;
                x[i] += t1;
                y[i] += t2;
            }
            z = u1 * c1 - u2 * c2;
            u2 = u1 * c2 + u2 * c1;
            u1 = z;
        }
        c2 = Math.Sqrt((1.0 - c1) / 2.0);
        if (direction == Direction.Forward) 
            c2 = -c2;
        c1 = Math.Sqrt((1.0 + c1) / 2.0);
    }

    /* Scaling for forward transform */
    if (direction == Direction.Forward)
    {
        for (i = 0; i < n; ++i)
        {
            x[i] /= n;
            y[i] /= n;
            if (Math.Abs(x[i]) > data.ExtremeReal)
            {
                data.ExtremeReal = x[i];
                data.IndexExtremeReal = (int)i;
            }
            if (Math.Abs(y[i]) > data.ExtremeImag)
            {
                data.ExtremeImag = y[i];
                data.IndexExtremeImag = (int)i;
            }
        }
    }
}

FFT http://www.rghware.com/fft.png

我通过在我的应用程序中选择“Native DLL FFT”选项,在图表中间部分创建了CPU下降。

http://www.rghware.com/InstrumentTuner.zip(源代码)

我认为这将在大多数PC上运行。 您需要安装DirectX。我使用某些硬件的捕获设置遇到了一些问题。 捕获设置应该是可配置的,但是应用程序的开发已因此有趣的发现而受到干扰。

无论如何,为什么我使用本地代码会看到速度提高20%? 这似乎与我先前的一些假设相悖。

更新

将函数转换为不安全方法并修复长整型问题后,新的不安全方法实际上比本地方法运行得更快(非常酷)。

Profile http://www.rghware.com/profile.png

很明显,数组边界检查是此FFT方法减速20%的原因。 由于它的性质,此方法中的for循环无法优化。

感谢大家的帮助。


我再次上传了源代码(这次包括类库)。 - Robert H.
1
你是如何进行速度比较测试的?你是否在调试器之外以发布模式运行,并且多次运行(以确保没有任何JIT问题)? - Reed Copsey
我正在IDE之外的发布模式下运行它。我使用System.Diagnostics.Stopwatch来测试这些函数的速度。我将结果放在表格上,以便通过单选按钮来观察和切换。该函数基本上在半秒间隔内不断地对进入声卡的数据执行。我已经进行了许多次测试。 - Robert H.
1
@Robert Hamilton:尝试将JetBrains从采样分析器切换到跟踪分析器。它会运行得慢得多,但您可以获得有关瓶颈的非常不同的信息水平。AQTime是另一种仅限跟踪的分析器(实际上我更喜欢),适用于这种类型的工作。此外,我的最后一个建议(请参见我的答案)有帮助吗? - Reed Copsey
抱歉,网站隐藏了其中一个评论。但是,你在长/整数问题上完全击中了它。我已经更新了帖子并附上了结果。 - Robert H.
显示剩余3条评论
7个回答

23

仅从这段代码来看,根据我的经验,从C++转到C#会导致相当大的减速。

在将这样的例程进行朴素的C#移植时,你将面临一个主要问题,即C#将在每个数组检查上添加边界检查。由于你没有以能得到优化的方式循环访问数组,(详情请参见此问题),因此几乎每个数组访问都会接收到边界检查。

另外,这个移植非常接近于从C进行1对1映射。如果你将其通过一个良好的.NET分析器运行,你可能会发现一些可以优化的地方,并通过一两个调整使其回到接近C++的速度(这几乎总是我在移植类似例程时的经验)。

然而,如果你想使其速度几乎相同,你可能需要将其转换为不安全的代码并使用指针操作代替直接设置数组。这将消除所有边界检查问题,并使速度恢复正常。


编辑:我发现另外一个巨大的区别,这可能是你的C#不安全代码运行更慢的原因。

请查看有关C#与C++的这个页面,特别是:

“long类型:在C#中,long类型为64位,而在C++中,它为32位。”

你应该将C#版本转换为使用int,而不是long。在C#中,long是一个64位类型。这实际上可能会对你的指针操作产生深远的影响,因为我认为你无意中在每个指针调用上添加了长整型->整型转换(带溢出检查)。

此外,当你在处理这个问题时,你可能想尝试将整个函数放入unchecked块中。C++没有像C#那样进行溢出检查。


Rico Mariani 关于边界检查:http://blogs.msdn.com/ricom/archive/2006/07/12/663642.aspx - VVS
@David:我喜欢Rico Mariani的博客,但在这种情况下,这不是一个公平的比较。他比较的是典型GUI应用程序的速度降低,而不是纯数值计算情况。即使在这种情况下,他也看到了由于数组边界检查而导致的总体3%的降低。然而,这个例程几乎全部都是数组访问,因此这个数字不会被其他计算所缓冲。如果你去掉最低的10%,我预计会有比3%更高的降低。 - Reed Copsey
我将我的代码转换为使用指针来操作数据的不安全方法(请参见问题正文中的最新编辑)。结果并不是我所期望的。我的不安全实现有什么问题吗?如果需要,我可以发布新代码。 - Robert H.
@Robert Hamilton:请看我的编辑。我认为“long”声明在这里改变了行为。 - Reed Copsey
@Reed Copsey:好的决定!通过长整型进行指针操作是问题所在。我将不安全方法中的内容更改为int,现在运行速度非常快。我已经更新了帖子,并附上了新的分析结果——令人印象深刻。 - Robert H.
很高兴我们解决了这个问题...这与我过去的经验相符。 :) - Reed Copsey

3

这很可能是由于JIT编译器生成的代码不如本地编译器生成的代码高效。

如果您关心性能下降了20%,则可以通过对代码进行分析来解决问题,或者考虑使用已优化的现成库。


3

本地编译器可以进行比JIT编译器更深入和更重的优化,如矢量化、过程间分析等。而使用矢量化技术可以大幅提高FFT速度。


1

我刚刚运行了他发布的代码,使用int而不是long,并没有什么区别。我知道其他人在.NET中的FFT方面有更好的运气,这表明.NET甚至可以在FFT数学方面达到或超过C++的性能。

所以我的答案是,要么发布者的代码比链接中的代码更优化(针对C),要么它在C#上的优化程度不如我提供的文章中的代码。

I performed two sets of tests on two machines with .NET 2.0. One machine had XPSP2 and had a single processor, 850MHz Pentium III, with 512Mb of RAM. The other machine had build 5321 of Vista and had a single processor, 2 GHz Mobile Pentium 4, with 1Gb of RAM. In each case I calculated the average of 100 separate FFT calculations on 217 (131072) data values. From these values I calculated the standard error from the standard deviation.

The results are shown in ms. The results for the Pentium III machine are:

  Not Optimized   Optimized For Space Optimized For Speed
Unmanaged   92.88 ± 0.09    88.23 ± 0.09    68.48 ± 0.03
Managed C++ 72.89 ± 0.03    72.26 ± 0.04    71.35 ± 0.06
C++/CLI 73.00 ± 0.05    72.32 ± 0.03    71.44 ± 0.04
C# Managed  72.21 ± 0.04    69.97 ± 0.08

The results for the Mobile Pentium 4 are:

          Not Optimized   Optimized For Space Optimized For Speed
Unmanaged   45.2 ± 0.1  30.04 ± 0.04    23.06 ± 0.04
Managed C++ 23.5 ± 0.1  23.17 ± 0.08    23.36 ± 0.07
C++/CLI 23.5 ± 0.1  23.11 ± 0.07    23.80 ± 0.05
C# Managed  23.7 ± 0.1  22.78 ± 0.03

我似乎无法复制这个:本机代码的平均值为0.007396秒+/- 0.000017,托管C#代码的平均值为0.008422秒+/- 0.000027。在我的机器上,使用本机代码仍然可以获得18-19%的速度提升(他的机器不是我的)。我将把它放在我的笔记本电脑上,看看是否有任何区别。 - Robert H.
让我想知道操作系统和处理器设计的改进对这些数字总体上有什么影响。 - JasonRShaver

1
考虑到托管代码对每个数组访问的索引进行边界检查,而非托管代码则不会这样做,我认为差异比我预期的要小。
如果你在托管代码中也将数组改为指针(因为在非托管代码中它们实际上就是指针),我预计它们的性能应该差不多。

请注意,有些情况下可以优化掉数组边界检查(例如当限制条件明确小于数组长度时),但本例不适用。 - Lucas

0

0
你是否使用诸如AQTime之类的分析工具来查看瓶颈在哪里?有时从本地代码转换到托管代码时可能是一件微不足道的事情。另一方面,由于在某些情况下托管代码比本地代码慢,因此您可能想尝试使用不安全代码。

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