整数开方算法

1 目的

  本文阐述了常用的开方算法的原理,重点描述了整数开方算法的实现并给出实例源码,旨在提高不带FPU的处理器处理开发的效率。

2 常用的开方算法

2.1 逼近法

        2.1.1 二分法逼近

                选取一个平方值大于目标值a的值b,和一个小于目标值的值c,取中间值d=(b+c)/2的平方e与a比较,若偏大,设置b的新值为d,否则设置c的新值为d,直至d的平方等于a或者b和c相等位置。这种猜测操作简单,但迭代次数过多,这里不详细描述,亦不给出源码。

        2.1.2 牛顿法逼近

                我们知道,在二维曲线的上的一点,沿着切线方向的变化速度最大。所以,如果我们确定了一个函数g(x),并且知道了它的目标值a对应的x的值,求g(x)=a等效于求g(x)-a的零点。取起点为x0,只考虑泰勒展开的前面两项,就是f(x)=g(x)-a=0的切线,亦即f(x0)+f'(x0)*(x-x0)=0,那么x=x0-f(x0)/f'(x0),对于开方,f(x)=x^2,那么,我们求的x=x0-(x0^2-a)/(2*x0)=(x0+a/x0)/2,再以x为新起点不断迭代,直至x和x0相等或者x的平方等于a为止。

2.2 数学法分离

       类似于我们数学的常规思维,以10机制为例,我们知道,每一位的平方的结果,要么是个一位数(eg.2x2=4),要么是个两位数(eg.7x7=49),我们把一个数两两分组,先猜最高位组,剩下的余数落入次高位组,再尝试去猜次高位组。例如,我们假设一个数abcd,若k^2=abcd,那么k应该是个2位数,假设k=10*x+y,那么abcd=(100*x^2+20*x*y+y^2),我们先猜出了x,那么abcd-100*x^2=20*x*y+y^2,也就是通过ab猜出x后,将余数放进cd的结果是20*x*y+y^2,我们知道了x,下一步要猜的数就是y,同样,在0到9范围内猜出不大于20*x*y+y^2的数,然后继续下一步。注意,这里进行迭代的x不只是上次x的那位,而是之前已经算出的根x,这样才符合k=10*x+y这个条件(eg.已依次算出y0=1,y1=2,那么引进新的2位作为余数增加项来算y3时,k=120+y3,abcd00-12*12*100+ef=20*12*y+y^2,余数为240y+y^2)。

       因为要猜很多次,所以,一般来说,这种十进制算法仅限于心算。但是,对于计算机来说,我们可以将10进制数改成2进制数,两位两位地猜,二进制数只有0值和1值,我们将1值代进去猜测即可,只需要猜1次。一个32位数,我们猜16次,就可以得到结果。对于二进制数来说,若k^2=abcd,假设k=2*x+y,那么abcd=4*x^2+4*x*y+y,ab的余数放到cd里面的结果是:abcd-4*x^2=4*x*y+y,而4这个乘法因子,是可以通过移位来完成的。

3 实现

3.1 牛顿法逼近

   牛顿法逼近,选取合适的初始值很重要,如果初始值很接近结果的话,迭代的次数会变得很少。

   对于一个数b=(1+a)*2^n(其中a为小数部分),如果,n为偶数,即n=2*m,那么b=(1+a)*2^(2*m)的开方c=2^m*(1+a)^0.5,此时,如果我们可以快速地获取到n的值,我们就可以拿到一个比较接近目标值的数了;另外,注意到(1+a)=(1+a/2)^2-a^2/4,因为0

   对于b=(1+a)*2^n,n为奇数来说,又应该怎么处理呢?其实,我们可以把其中的一个2拿出来开方,再乘上偶数的结果即可。

   那么,有没有方法可以快速拿到a和n的值呢?其实是有的,浮点数存储的就是这两个东西,0~22位存储的刚好是a,23~30位存储的是n+127,31位存储的符号位。所以,我们拿初始值的函数代码为:

  

int IntSqrt_Init(float f)
{
	uint32_t ret;
	uint32_t data = *(uint32_t *)&f;
	const int exp = ((data >> 23) & (0x0ff)) - 127;
	const int tail = data & ((1 << 23) - 1);
	ret = (((exp >> 1) + 127) << 23) | (tail >> 1);
	ret =  *(float *)&ret;
	if(exp & 0x01)
		ret += ret >> 1;
	return ret;
}

    全程都是位运算和加减法,速度是很快的。

   下面是牛顿法的代码:

    

unsigned Int_Sqrt(unsigned y)
{
    unsigned x[2];
    if(y <= 1)
      return y;
    x[0] = IntSqrt_Init(y);
    x[1] = x[0] + (x[0] >> 1);
    while(x[0] != x[1])
    {
        x[1] = x[0];
        x[0] = (y / x[0] + x[0]) >> 1;
    }
    return x[0];
}

    有一个除法运算,它的用时是比较长的,但因为初始值已比较接近目标值,迭代次数比较少,一般两三次就可以达到目标值,所以,总体来看还是很快的。

3.2 数学法分离

 

uint16_t BinSqrt(uint32_t value)
{
    uint16_t root;
    uint16_t rem;
    int i;
    for(root = rem = i = 0; i < 16; ++i)
    {
        int if1;
        root <<= 1;
        rem = (rem << 2) | (value >> 30);
        value <<= 2;
        if1 = (root << 1) + 1;
        if(rem >= if1)
        {
            root |= 1;
            rem -= if1;
        }
    }
    return root;
}

  全程位运算和加减法,循环内部速度快,不过循环体要固定执行16次,总体上来看,还是挺快的。

4 效率测试

   在不带FPU,硬件除法器的MCU(CM0核)上面测试,两者效率相差无几,均高于系统自带的sqrt函数,用时在sqrt的一半一下。如果有硬件除法器的话(如CM3核),是整数牛顿法更优。

   据说雷神之锤的开方算法效率更高,但是,那个开方算法用到了几个浮点数运算,并且还是算开方的倒数的,最后还要做一个浮点数除法,一路下来,对于CM0核的整数开方来说,它的效率虽然高于sqrt,但不及上面两种算法。所以,雷神之锤的开方算法,更适用于带FPU的芯片(如CM4核),效率会很高。

你可能感兴趣的:(数学;理论;,c语言)