如何改进这个平方根方法?

时间:2009-05-13 05:34:46

标签: c# algorithm optimization math performance

我知道这听起来像是家庭作业,但事实并非如此。最近我一直对用于执行某些数学运算的算法感兴趣,例如正弦,平方根等。目前,我正在尝试用C#编写Babylonian method计算平方根。

到目前为止,我有这个:

public static double SquareRoot(double x) {
    if (x == 0) return 0;

    double r = x / 2; // this is inefficient, but I can't find a better way
                      // to get a close estimate for the starting value of r
    double last = 0;
    int maxIters = 100;

    for (int i = 0; i < maxIters; i++) {
        r = (r + x / r) / 2;
        if (r == last)
            break;
        last = r;
    }

    return r;
}

它工作得很好,每次都会产生与.NET Framework的Math.Sqrt()方法完全相同的答案。然而,正如你可能猜到的那样,它比原生方法慢(大约800个滴答)。我知道这个特殊的方法永远不会比原生方法快,但我只是想知道我是否有任何优化。

我立即看到的唯一优化是计算运行100次,即使已经确定了答案(此时r将始终是相同的值)。因此,我添加了一个快速检查,以查看新计算的值是否与先前计算的值相同并突破循环。不幸的是,它在速度方面没有太大差别,但似乎是正确的做法。

在您说“为什么不使用Math.Sqrt()之前?”......我这样做是为了学习练习,并不打算在任何生产代码中实际使用此方法。

12 个答案:

答案 0 :(得分:6)

首先,不应检查相等性(r == last),而应检查收敛性,其中r接近last,其中close由任意epsilon定义:

eps = 1e-10  // pick any small number
if (Math.Abs(r-last) < eps) break;

作为与提及相关联的维基百科文章 - 您无法使用牛顿方法有效地计算平方根 - 而是使用对数。

答案 1 :(得分:5)

float InvSqrt (float x){
  float xhalf = 0.5f*x;
  int i = *(int*)&x;
  i = 0x5f3759df - (i>>1);
  x = *(float*)&i;
  x = x*(1.5f - xhalf*x*x);
  return x;}

这是我最喜欢的快速平方根。实际上它是平方根的倒数,但你可以在你想要之后将其反转......我不能说如果你想要平方根而不是平方根,那么它是否更快,但它的变形很酷。
http://www.beyond3d.com/content/articles/8/

答案 2 :(得分:4)

你在这里做的是执行Newton's method of finding a root。所以你可以使用一些更有效的根寻找算法。您可以开始搜索here

答案 3 :(得分:2)

而不是打破循环然后返回r,你可以返回r。可能无法提供任何显着的性能提升。

答案 4 :(得分:2)

用一个位移替换除法2不太可能产生那么大的差异;鉴于该除法是一个常数,我希望编译器足够聪明,可以为你做到这一点,但你也可以尝试一下。

你更有可能通过提前退出循环来获得改进,因此要么将新r存储在变量中并与旧r进行比较,要么将x / r存储在变量中并在执行之前将其与r进行比较加法和分裂。

答案 5 :(得分:2)

使用您的方法,每次迭代都会使正确的位数加倍。

使用表来获取最初的4位(例如),第一次迭代后将有8位,第二次后将有16位,以及第四次迭代后需要的所有位(因为{{1}存储52 + 1位尾数)。

对于表查找,您可以在[0.5,1 [和输入中的指数(使用类似frexp的函数))中提取尾数,然后在[64,256 [使用乘以适当的2的幂]中对尾数进行归一化。 / p>

double

此后,您的输入数字仍为mantissa *= 2^K exponent -= K 。 K必须是7或8才能获得偶数指数。您可以从包含尾数的整数部分的所有平方根的表中获取迭代的初始值。执行4次迭代以获得尾数的平方根r。结果是mantissa*2^exponent,使用r*2^(exponent/2)等函数构建。

EDIT。我在下面放了一些C ++代码来说明这一点。具有改进测试的OP函数sr1需要2.78秒来计算2 ^ 24平方根;我的函数sr2需要1.42s,硬件sqrt需要0.12s。

ldexp

答案 6 :(得分:1)

定义公差并在后续迭代落在该公差范围内时提前返回。

答案 7 :(得分:1)

由于你说下面的代码不够快,试试这个:

    static double guess(double n)
    {
        return Math.Pow(10, Math.Log10(n) / 2);
    }

它应该非常准确,并且希望很快。

以下是here描述的初始估算的代码。它似乎很不错。使用此代码,然后您还应该迭代,直到值收敛于差异的epsilon。

    public static double digits(double x)
    {
        double n = Math.Floor(x);
        double d;

        if (d >= 1.0)
        {
            for (d = 1; n >= 1.0; ++d)
            {
                n = n / 10;
            }
        }
        else
        {
            for (d = 1; n < 1.0; ++d)
            {
                n = n * 10;
            }
        }


        return d;
    }

    public static double guess(double x)
    {
        double output;
        double d = Program.digits(x);

        if (d % 2 == 0)
        {
            output = 6*Math.Pow(10, (d - 2) / 2);
        }
        else
        {
            output = 2*Math.Pow(10, (d - 1) / 2);
        }

        return output;
    }

答案 8 :(得分:1)

我一直在考虑这个以用于学习目的。您可能对我尝试过的两个修改感兴趣。

第一种是在x0中使用一阶泰勒级数近似:

    Func<double, double> fNewton = (b) =>
    {
        // Use first order taylor expansion for initial guess
        // http://www27.wolframalpha.com/input/?i=series+expansion+x^.5
        double x0 = 1 + (b - 1) / 2;
        double xn = x0;
        do
        {
            x0 = xn;
            xn = (x0 + b / x0) / 2;
        } while (Math.Abs(xn - x0) > Double.Epsilon);
        return xn;
    };

第二个是尝试第三个订单(更昂贵),迭代

    Func<double, double> fNewtonThird = (b) =>
    {
        double x0 = b/2;
        double xn = x0;
        do
        {
            x0 = xn;
            xn = (x0*(x0*x0+3*b))/(3*x0*x0+b);
        } while (Math.Abs(xn - x0) > Double.Epsilon);
        return xn;
    };

我创建了一个辅助方法来计算函数的时间

public static class Helper
{
    public static long Time(
        this Func<double, double> f,
        double testValue)
    {
        int imax = 120000;
        double avg = 0.0;
        Stopwatch st = new Stopwatch();
        for (int i = 0; i < imax; i++)
        {
            // note the timing is strictly on the function
            st.Start();
            var t = f(testValue);
            st.Stop();
            avg = (avg * i + t) / (i + 1);
        }
        Console.WriteLine("Average Val: {0}",avg);
        return st.ElapsedTicks/imax;
    }
}

原始方法更快,但同样可能有趣:)

答案 9 :(得分:1)

用“* 0.5”替换“/ 2”会使我的机器速度提高1.5倍,但当然不如原生实现快。

答案 10 :(得分:0)

好吧,本机Sqrt()函数可能没有在C#中实现,它很可能是用低级语言完成的,它肯定会使用更高效的算法。所以试图匹配它的速度可能是徒劳的。

然而,关于只是尝试优化你的heckuvit函数,你链接的维基百科页面建议“开始猜测”为2 ^ floor(D / 2),其中D代表二进制数字的数字。数。你可以尝试一下,我没有看到其他许多可以在你的代码中显着优化的内容。

答案 11 :(得分:-2)

你可以试试 r = x >> 1;

而不是/ 2(在你设备的另一个地方也是2)。 它可能会给你一点点优势。 我还会将100移动到循环中。可能没什么,但我们在这里讨论滴答声。

现在就检查一下。

编辑: 修正了&gt;进入&gt;&gt;,但它不适用于双打,所以没关系。 100的内联没有给我增加速度。