高效率嵌入式系统开平方根.docx

上传人:b****7 文档编号:11391343 上传时间:2023-02-28 格式:DOCX 页数:16 大小:32.40KB
下载 相关 举报
高效率嵌入式系统开平方根.docx_第1页
第1页 / 共16页
高效率嵌入式系统开平方根.docx_第2页
第2页 / 共16页
高效率嵌入式系统开平方根.docx_第3页
第3页 / 共16页
高效率嵌入式系统开平方根.docx_第4页
第4页 / 共16页
高效率嵌入式系统开平方根.docx_第5页
第5页 / 共16页
点击查看更多>>
下载资源
资源描述

高效率嵌入式系统开平方根.docx

《高效率嵌入式系统开平方根.docx》由会员分享,可在线阅读,更多相关《高效率嵌入式系统开平方根.docx(16页珍藏版)》请在冰豆网上搜索。

高效率嵌入式系统开平方根.docx

高效率嵌入式系统开平方根

开平方根

1.开平方根

2.开平方根说明

错误!

未指定书签。

错误!

未指定书签。

 

1.开平方根

我们平时经常会有一些数据运算的操作,需要调用,,等函数,那么时候你有没有想过:

个些函数系统是如何实现的?

就拿最常用的函数来说吧,系统怎么来实现这个经常调用的函数呢?

虽然有可能你平时没有想过这个问题,不过正所谓是临阵磨枪,不快也光”你眉头一皱,

计上心来”这个不是太简单了嘛,用二分的方法,在一个区间中,每次拿中间数的平方来试验,如果大了,就再试左区间的中间数;如果小了,就再拿右区间的中间数来试。

比如求(16)的结果,

你先试(0+16)/2=8,8*8=64,64比16大,然后就向左移,试(0+8)/2=4,4*4=16

刚好,你得到了正确的结果(16)=4。

然后你三下五除二就把程序写岀来了:

1.开平方根

我们平时经常会有一些数据运算的操作,需要调用,,等函数,那么时候你有没有想过:

这个些函数系统是如何实现的?

就拿最常用的函数来说吧,系统怎么来实现这个经常调用的函数呢?

虽然有可能你平时没有想过这个问题,不过正所谓是“临阵磨枪,不快也光”,你“眉头一皱,计上心来”,这个不是太简单了嘛,用二分的方法,在一个区间中,每次拿中间数的平方来试验,如果大了,就再试左区间的中间数;如果小了,就再拿右区间的中间数来试。

比如求(16)的结果,你先试(0+16)/2=8,8*8=64,64比16大,然后就向左移,试(0+8)/2=4,4*4=16刚好,你得到了正确的结果(16)=4。

然后你三下五除二就把程序写出来了:

用二分法

(n)

{

小于0的按照你需要的处理

(n<0)

n;

0;

()/2;

{

(*>n)

()/2;

}

精度控制

1.开平方根

我们平时经常会有一些数据运算的操作,需要调用,,等函数,那么时候你有没有想过:

这个些函数系统是如何实现的?

就拿最常用的函数来说吧,系统怎么来实现这个经常调用的函数呢?

虽然有可能你平时没有想过这个问题,不过正所谓是“临阵磨枪,不快也光”,你“眉头一皱,计上心来”,这个不是太简单了嘛,用二分的方法,在一个区间中,每次拿中间数的平方来试验,如果大了,就再试左区间的中间数;如果小了,就再拿右区间的中间数来试。

比如求(16)的结果,你先试(0+16)/2=8,8*8=64,64比16大,然后就向左移,试(0+8)/2=4,4*4=16刚好,你得到了正确的结果(16)=4。

然后你三下五除二就把程序写出来了:

用二分法

(n)

{

小于0的按照你需要的处理

(n<0)

n;

0;

()/2;

{

(*>n)

()/2;

}

精度控制

的每条运算指令。

当初的3D也得听取他的意见,修改了不少。

最近,的开发商遵守协议,公开了的原代码,让世人有幸目睹传奇的3D引擎的原码。

是原代码的下载地址:

我们知道,越底层的函数,调用越频繁。

3D引擎归根到底还是数学运

算。

那么找到最底层的数学运算函数(在),必然是精心编写的。

里面有很多有趣的函数,很多

都令人惊奇,估计我们几年时间都学不完。

在里发现了这样一段代码。

它的作用是将一个数开平方并取倒,经测试这段代码比()(1.0(x))快4倍:

()

{

i;

x2,y;

=1.5F;

x2=*0.5F;

y=;

i=*(*);

i=0x5f3759-(i>>1);?

y=*(*);

y=y*(-(x2*y*y));1

y=y*(-(x2*y*y));2,

Q3

((y));010122-?

y;

}

函数返回1(x),这个函数在图像处理中比(x)更有用。

注意到这个函数只用了一次叠代!

(其

实就是根本没用叠代,直接运算)。

编译,实验,这个函数不仅工作的很好,而且比标准的()函数

快4倍!

要知道,编译器自带的函数,可是经过严格仔细的汇编优化的啊!

这个简洁的函数,最核心,也是最让人费解的,就是标注了“?

”的一句:

i=0x5f3759-

(i>>1);

再加上y=y*(-(x2*y*y));

两句话就完成了开方运算!

而且注意到,核心那句是定点移位运算,速度极快!

特别在很多

没有乘法指令的结构上,这样做是极其高效的。

算法的原理其实不复杂,就是牛顿迭代法,用(x)'(x)来不断的逼近f(x)的根。

没错,一般的求平方根都是这么循环迭代算的但是卡马克(3作者)真正牛B的地方是他选择

了一个神秘的常数0x5f3759来计算那个猜测值,就是我们加注释的那一行,那一行算岀的值非

常接近1(n),这样我们只需要2次牛顿迭代就可以达到我们所需要的精度。

好吧如果这个还不算,

普渡大学的数学家看了以后觉得有趣,决定要研究一下卡马克弄岀来的这个猜测值有什么

奥秘。

也是个牛人,在精心研究之后从理论上也推导出一个最佳猜测值,和卡马克的数字非常接

近,0x5f37642f。

卡马克真牛,他是外星人吗?

传奇并没有在这里结束。

计算出结果以后非常满意,于是拿自己计算出的起始值和卡马克的

神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。

结果是卡马克赢了…谁也不知道

卡马克是怎么找到这个数字的。

最后怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数字要好上那么

一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴力得岀的数字是

0x5f375a86。

为此写下一篇论文,““。

论文下载地址:

,。

最后,给岀最精简的1()函数:

这个简洁的函数,最核心,也是最让人费解的,就是标注了“?

”的一句:

i=0x5f3759-

(i>>1);

再加上y=y*(-(x2*y*y));

两句话就完成了开方运算!

而且注意到,核心那句是定点移位运算,速度极快!

特别在很多没有乘法指令的结构上,这样做是极其高效的。

算法的原理其实不复杂,就是牛顿迭代法,用(x)'(x)来不断的逼近f(x)的根。

没错,一般的求平方根都是这么循环迭代算的但是卡马克(3作者)真正牛B的地方是他选择

了一个神秘的常数0x5f3759来计算那个猜测值,就是我们加注释的那一行,那一行算岀的值非

常接近1(n),这样我们只需要2次牛顿迭代就可以达到我们所需要的精度。

好吧如果这个还不算,

普渡大学的数学家看了以后觉得有趣,决定要研究一下卡马克弄岀来的这个猜测值有什么奥秘。

也是个牛人,在精心研究之后从理论上也推导岀一个最佳猜测值,和卡马克的数字非常接近,0x5f37642f。

卡马克真牛,他是外星人吗?

传奇并没有在这里结束。

计算岀结果以后非常满意,于是拿自己计算岀的起始值和卡马克的神秘数字做比赛,看看谁的数字能够更快更精确的求得平方根。

结果是卡马克赢了...谁也不知道卡马克是怎么找到这个数字的。

最后怒了,采用暴力方法一个数字一个数字试过来,终于找到一个比卡马克数字要好上那么一丁点的数字,虽然实际上这两个数字所产生的结果非常近似,这个暴力得岀的数字是0x5f375a86。

为此写下一篇论文,""。

论文下载地址:

,。

最后,给岀最精简的1()函数:

(x){

=0.5f*x;

2.开平方根说明

人们很早就在3源代码中发现了类似如下的C代码,它可以快速的求1(x),在3D图

形向量计算方面应用很广

(x)

{

=0.5*x;

i=*(*);

i=0x5f3759-(i>>1);

x=*(*);

x=x*(1.5-*x*x);

x;

}

在分析这段代码之前,先看看传统方法是怎么求一个数的平方根的倒数的,一般采用

牛顿迭代法,为描述方便,假设输入数为a,显然需要满足a>0,是C语言的求平方

根函数,为方便起见,下面用(x)的形式代替xA(1/2)

求1(a),用迭代法,即求方程f(x)A(-2)在f(x)=0时的解,选择适当的初始值x0,代

入迭代式:

(x)/f'(x)

化简此式得:

32八3/2

这实际上就是上面函数倒数第二行,从函数注释也可以直接看出,这一步就是牛顿迭

代,一般选择一个合适的初始值开始迭代后,迭代次数越多越接近解,换句话说就是精度越高,误差越小,当误差小于可接受值,即可获得近似结果了,就这个问题而言,初始值的选择一般要在区间(0,(3)),证明从略而这个函数厉害的地方就在于,在正式迭代开始前的三行计算已经得到了一个非常接近于解的数,因此只需一次迭代,即可得到近似值,经测试,对于常用的浮点数范围,(x)与标准解1(x)的最大相对误差为1.75%0,平均相对误差为0.95%0,这个精度在很多时候已经满足的基本要求1%。

了,而(x)的速度则比直接计算1(x)快4倍,这对于和这类游戏的性能是非常重要的,而且如果需要更高的精度,则将迭代那一行再重复一次就可以了,相对误差会降到百万分之一的级别,只不过速度会慢一些,接下来我们来分析下这三行代码的原理

最令人迷惑的是这行,即对i做了移位和减法运算,不过熟悉C语言的人应该能看

出来,这个算法和浮点数的内部表示有关,分析应该从这里入手正式开始前先轻松下,讲些历史故事,人们在源码发现了这个函数,于是很自然的认为这是卡马克()的杰作,其中0x5f3759这个数被称为卡马克密码,我们在下面称这个数为,3的一直在想到底是哪个家伙写了这些神奇的代码,于是就开始找作者,在邮件回复中明确表示不是他,也不是。

说他写过类似的高效代码,但上面的不是。

后来猜测这个来自于一些早期黑客的算法笔记,作者究竟是谁自然也难以追查了,可以肯定的是这个家伙对计算机和高数知识都有较好理解,很聪明

2003年普渡大学的数学家写了一篇文章对这段代码进行了分析。

论文是英文的,地址在:

在这篇12页的论文中,对这个算法做了分析,并从推导出了一个理论上最优的

0x5f37642f,有意思的是,这个数居然没有里的0x5f3759效果好,最大相对误差达

到1.78%。

,一怒之下,用暴力搜索枚举了所有可能的,终于找到一个最优的

0x5f375a86,只比0x5f3759效果好一点点,至于的作者究竟如何找到0x5f3759的,也就是个迷了

开始正式分析,这三行代码是把在内存中的表示作为一个整数i看待,然后对i进行一次移位和减法,然后再将i的值作为一个看待,所以我们先看看在内存中的表示,一般计算机的浮点数遵循754标准,采用以2为底数的科学计数法,例如二进制的11010.11001记为1.101011001*10X00,和都是32位,占4个字节(注意,这

个函数早期的代码中整数类型应该是,因为那时候在下,是16位的):

最高位d31:

符号位,0表示非负,1表示非正,为什么不直接说正负呢,因为有数值0的存在,浮点数有+0和-0的区别,这个位用S表示d3023:

指数域,存放一个整数,表示127,E为指数,由于指数域的范围是0~255,因此理论上可以表示的指数范围是-127~128,不过0和255有特殊含义,所以范围实际要稍微小一点,这个先按下不表,我们认为常用浮点数不包括这两种极端情况d220:

有效数字域,只是小数部分,由于科学计数法的规定,整数部分肯定是1,就省略了,这样可以避免不必要的精度浪费,为描述方便,这个域所表示的小数设为F。

当然有人会问,那0怎么办,+0和-0有自身的特殊表示法,S位表示符号,其他位都为0的时候是+0或-0于是除去0和754规定的特殊值,一个常用浮点数的表示可以看做:

(-1)AS*

(1)*2AE,具体到我们需要分析的问题,由于输入是正数,S位肯定是0,就不做考虑了,简化为:

(1)*2AE

好,现在我们需要求

(1)*2AE的平方根的倒数,即求1(

(1)*2AE),求得的结果当然也

要用这个浮点数表示法,有效数字必须在[1,2),指数域为整数,则结果分两种情况推导出结果:

E为奇数:

(1))*2八(-

(1)/2)

E为偶数:

2

(1)*2八(2-1)

先看指数,如果我们需要通过计算机的整数运算(移位、位运算和加减法等)来逼近解,首先要在数量级上尽量靠近,因为只要数量级一样,两个数的误差范围是最小的,也就是说,我们需要:

E为奇数时,将127变成127-

(1)/2

E为偶数时,将127变成1272-1

于是,通过右移一位来实现除以二,通过用一个数减去指数域来将E变成负的,这样

中的那句就很容易理解了:

E为奇数时,127为偶数,右移等于除以二,190-(127)/2=190-63-

(1)/2=127-

(1)/2

E为偶数时,127为奇数,右移等于先减一再除以二,189-(1271)/2=189-632=

1272-1

然后我们把中的0x5f3759用的形式展开,则其指数域为0x5f<<1,也就是190,那当E在偶数时怎么生成189呢,注意到当E为偶数时,i>>1这个操作将指数域最后一位的1(即上面说的“先减一”)也向右移了一位,i右移后d22位为1,这样一来只要被减数的d22这一位是0,就会因为不够减而产生借位,指数域被借了1,自然就变成189了

细心的童鞋应该发现了,当E为偶数时,减法做完后,指数域一定是1272-1,但是如果E为奇数,则不保证指数域是127-

(1)/2,因为这时候被减数和减数的d22位都是0,但如果减数的d21到d0这个域的数字比被减数的大,就会产生借位而使得指

数域比预期的127-

(1)/2要小1,这个问题会导致一定的误差,但是在最后的迭代中误差会被缩小,这个误差具体有多大,这里就不详细讨论了,有兴趣的童鞋可以自己

算算看

我们还是先证明用这种方法得到的x0是落在上述区间(0,(3))的,这里a就是

(1)*29由于输入是一个非负数,则S为0,指数域肯定不为0(右移后不可能刚好为190),因此x0肯定大于0,我们将(3)展开成期望的解的指数的乘法形式:

E为奇数时,展开为⑹

(1))*2(

(1)/2),如果按上面说的那种情况产生借位,则展开

为(24/

(1))*2A(-(3)/2)

E为偶数时,展开为(12心))*2八(2-1)

可以看到,无论是哪种情况,在指数相同的情况下,有效数字都大于2,反过来说,用上述算法得到的x0是小于(3)的,而且还小了很多,非常接近解,这时候只需要一次迭代,就得到了误差很小的近似结果

到这里,基本原理都清楚了,只要我们保证x0在这个区间中,再做迭代总是能进一步接近解的,现在的问题就在于中d21到d0这个域的值应该怎么取了,这个取值关系到每次迭代的误差,比如说,我们全取1,这样也避免了E为奇数时的借位情况,

这样就是0x5f3,用这个测试,结果最大误差超过1%,平均误差超过6%。

,显然效

果太差了

我们假设将作为看时,小数部分的值是M,由于d22位已确定是0,则0<<0.5,在(i>>1)的减法运算中,d22到d0域的运算可看做是定点小数减法,分三种情况讨论:

E为奇数时,减数的d22位为0,则小数部分的值为2,因为右移对小数来说也是除以二,d0位如果是1则会舍弃,这个因为太小而忽略,假设M>2,则不需要向指数域借位,计算结果的小数部分为2,指数域符合结果预期,此时的相对误差为:

1()1-(12)(2/

(1))|,0<<=2M假设M<2,则需要向指数域借位,计算结果小数部分为12,指数位比预期低1,计算误差的时候需要将指数域差值补回去,此时的相对误差:

2()1-(22)/2(2/

(1))|,2M

E为偶数时,减数的d22位为1,则小数部分的值为2+1/2,此时必定借位,相对误差为:

3()1-(22-1/2)/2

(1)|,0<<1

然后,我们构造一个M的函数,映射M和此M下最大相对误差:

(M),这个函数的算法是对每个M,分别求1、2和3三个函数的最大值,并取其中最大的值。

于是,我们的最后问题就是找到一个M,使(M)最小,就得到最优了

用的推导方法类似,当然更严密些,推导到了这一步后,他的选择是继续用数学方法来求极值,这大概是因为他是个数学家的缘故。

不过悲剧的是他最后推出来的结果不是最优,计算机科学是一门应用科学,作为工科生,到这一步后我就准备选择穷举了,再往后的数学我就不懂了,毕竟哥的微积分才刚及格所以,让我们抛开那些高深的数学来看这个问题,通过前面的推导,我们基本可以将的范围确定在[0x5f000000,0x5f3],只要在这个范围内找到一个,使其对应的函数

对于所有常用浮点数的计算结果的最大相对误差最小,就可以了,用上面的话讲,就是()的最小值,由于取值是离散有限的,而所有常用浮点数个数也是有限的,所以至少理论上讲是可通过穷举计算,不过可惜两者的个数都太大,全部算一遍不切实际在的取值范围内均匀地取一些数,用统计的办法遍历所有常用浮点数计算(),可以看到曲线是一个波谷的样子,即前半段单调递减,后半段单调递增,于是我们近似地认为()的曲线就是个波谷,然后可以迭代求极值:

1定义和两个变量,表示取值范围[,],自然一开始为[0x5f000000,0x5f3]

2将范围平均分成32段(由于整数除法有误差,最后一段稍长),则有032,计算

33个值对应的(),找到其中的最小值对应的

3将

(1)和

(1)作为新的和,如果范围比较大则重复第2步,范围比较小则直接遍历范

围中的每个得到结果

经过若干次迭代,我们得到的最优为0x5f375a85,刚好比的最优结果小1当然了,这个方法虽然简单但是不够严密,我们没有证明()的曲线是先单调递减后单

调递增,也没有严格证明的取值在[0x5f000000,0x5f3]之外的话误差会更大,但我们毕竟是得到结果了,解决了应用问题

然后我们讨论下特殊的浮点数,输入一个0是无意义的,因为0不能做除数,但是对输入的0会返回一个比较大的结果,754规定的值和正负无穷在实际中都不太可能出现,需要注意的是当输入数非常小,例如指数域为0的时候,E为-127,这时候由于值很小,相对误差会达到很多倍,在实际应用中要尽量避免极小数字的计算

(a)返回1(a),如果我们需要快速地近似计算(a),可能很多人会自然而然想到1(a),

实际上,(a)*a更快,也更准确,因为在计算机中除法比乘法要慢很多,这也是(a)能比直接计算1(a)快那么多的原因,中没有除法运算,而的迭代不能避免除法,取倒数又得做一次除法(其实除法和取倒数也可以用迭代法来规避除法,但这又得有额外开销了)

用上面的办法可以推导出取倒数的迭代函数:

(x)

{

a=x;

i=0x7311c3-i;

x=*(*);

x=x*(2-a*x);

x;

}

但是这个效果比较令人失望,不但精度差,而且速度比直接算1慢,基本没用虽然可以通过(a)*a计算a的平方根,但是我们可以推导出一个更直接的函数:

(x)

{

a=x;

i=*(*);

i=(i+0x3f7662)>>1;

x=*(*);

x=(x+a/x)*0.5;

x;

}

推导过程从略,有兴趣的童鞋可以自行研究,注意这里是先加再右移,所以用防止

负数右移,这个计算平方根速度比(a)*a快,而且最大误差只有0.6%。

,那么,有没有更快的算法呢,如果单纯用计算,可能很难超越了,想要更快,得从另外的方向想办法

比较直接的想法是,造一个巨大的,储存a到(a)的映射关系,这样不需要计算,只要查表就行,而且由于指数域可以通过整数加减得到,只用做有效数字部分的映射即可,

内存消耗约为33M,这似乎是个好办法,而且当从小到大顺序生成常用浮点数是,其速度比稍慢一点点但是,如果我们随机生成常用浮点数,这个算法的耗时就惨不忍睹了,几乎是顺序生成浮点数时耗时的7倍,在实际应用中,每次计算输入的数都是随机的,这说明这个查表的办法没有实用价值,因为如果我们单纯的计算,浮点数运算是在协处理器中进行的,使用x87浮点数运算指令,整数运算则是在中,存储使用寄存器,非常快,而访问内存的速度是要慢很多的,那为什么顺序生成浮点数的时候速度没那么慢呢,因为这时候的一二三级发挥了巨大的作用,而随机生成的时候内存也要随机访问,则很高,速度自然慢了很多了所以说,查表法不可行的原因在于占用内存太大,我们要想办法将内存降下来,考虑函数(X),这是一条连续曲线,当然对于来讲,其实是离散的一个个点,数量还是有限的,查表法是每个点的值都记录,误差为0,那么我们可以考虑用牺牲精度的办法,将这条曲线近似成一条条线段组成的折线,然后在内存中记录每条线段的斜率,于是我们将[1,2)这个区间分散成若干段,事先计算每段的平均斜率,计算(a)的时候,先通过整数运算确定指数域的值,然后查表得到对应点的斜率,做一次浮点乘法即可得到近似值,为了保证运算速度,分段的段数需要保证用一次移位运算即可得到,避免整数除法(这个比浮点数除法更费时间):

(X)

{

a=X;

i=*(*);

i=(i+0X3f800000)>>1;

x=*(*);

x*=[(i&0x7)>>];

x;

}

其中是有效数字域右移的位数,储存平均斜率,当为16的时候,存放了128个整数,占内存512字节,已经足以放在一级里了(我的测试机器一级是双路64k,一般个人最少也有8k左右),而且此时的精度也还可以接受,越小,占内存越多,精度越高,不过,貌似比还是快不了太多

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工作范文 > 行政公文

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1