[转] 数值计算的优化
原帖已经打不开了,所以留着存根
tag:代码优化,除法,牛顿迭代,减法代替除法,除法优化
说明:文章中的很多数据可能在不同的CPU或不同的系统环境下有不同的结果,数据仅供参考
x86系列的CPU对于位运算、加、减等基本指令都能在1个CPU周期内完成(现在的CPU还能乱序执行,从而使指令的平均CPU周期更小);现在的CPU,做乘法也是很快的(需要几个CPU周期,每个周期可能启动一个新的乘指令(x87)),但作为基本指令的除法却超出很多人的预料,它是一条很慢的操作,整数和浮点的除法都慢;我测试的英特尔P5赛扬CPU浮点数的除法差不多是37个CPU周期,整数的除法是80个CPU周期,AMD2200+浮点数的除法差不多是21个CPU周期,整数的除法是40个CPU周期。(改变FPU运算精度对于除法无效)
(SSE指令集的低路单精度数除法指令DIVPS 18个CPU周期,四路单精度数除法指令DIVSS 36个CPU周期)
(x86求余运算和除法运算是用同一条CPU指令实现的;
据说,很多CPU的整数除法都是用数学协处理器的浮点除法器完成的;有一个推论就是,浮点除法和整数除法不能并行执行. (ps:intel的p4 imul指令可能有14周期(或15-18)的延迟才能得到结果)
本文将给出一些除法的优化方法或替代算法 (警告:某些替代算法并不能保证完全等价!)
1.尽量少用除法
比如: if (x/y>z) ...
改成: if (
((y>0)&&(x>y*z))||((y<0)&&(x<y*z)) ) ...
(少用求余)
比如: ++index; if (index>=count)
index=index % count; //assert(index<count*2);
改成: ++index; if
(index>=count) index=index - count;
2.用减法代替除法
如果知道被除数是除数的很小的倍数,那么可以用减法来代替除法
比如:
uint32 x=200;
? uint32 y=70;
uint32 z=x/y;
改成:
uint z=0;
while (x>=y)
{
x-=y;
++z;
}
一个用减法和移位完成的除法 (如果你没有除法指令可用
uint32
div(uint64 u,uint32 z) //return u/z
{
uint32
x=(uint32)(u>>32);
uint32 y=(uint32)u;
//y保存商
x保存余数
for (int i=0;i<32;++i)
{
uint32 t=((int32)x) >> 31;
x=(x<<1)|(y>>31);
y=y<<1;
if
((x|t)>=z)
{
x-=z;
++y;
}
}
return y;
}
(该函数经过了测试;z==0需要自己处理;对于有符号除法,可以用取绝对值的方法(当然也不是轻松就能
写出完全等价的有符号除法的;
如果不需s的64bit长度,仅需要32bit,那么可以化简这个函数,但改进不多)
3.用移位代替除法
(很多编译器能自动做好这个优化)
要求除数是2的次方的常量; (同理:对于某些应用,可以优先选取这样的数来做除数)
比如:
uint32 x=213432575;
? uint32 y=x/8;
改成:
y=x>>3;
对于有符号的整数;
比如: int32 x=213432575;
?
int32 y=x/8;
改成: if (x>=0) y=x>>3;
else
y=(x+(1<<3-1))>>3;
4.合并除法 (替代方法不等价,很多编译器都不会帮你做这种优化)
适用于不能用其它方法避免除法的时候;
比如: double x=a/b/c;
改成: double
x=a/(b*c);
比如: double x=a/b+c/b;
改成: double
x=(a+c)/b;
比如: double x=a/b;
double y=c/d;
double
z=e/f;
改成: double tmp=1.0/(b*d*f);
double
x=a*tmp*d*f;
double y=c*tmp*b*f;
double
z=e*tmp*b*d;
5.把除法占用的时间充分利用起来
CPU在做除法的时候,可以不用等待该结果(也就是后面插入的指令不使用该除法结果),而插入多条简单整数
指令(不包含整数除法,而且结果不能是一个全局或外部变量等),把除法占用的时间节约出来;
(当除法不可避免的时候,这个方法很有用)
6.用查表的方法代替除法
(适用于除数和被除数的可能的取值范围较小的情况,否则空间消耗太大)
比如 uint8 x; uint8 y;
?
uint8 z=x/y;
改成 uint8 z=table[x][y];
//其中table是预先计算好的表,table[j]=i/j;
//对于除零的情况需要根据你的应用来处理
或者:uint8 z=table[x<<8+y]; //其中table=(i>>8)/(i&(1<<8-1));
比如 uint8 x;
? uint8 z=x/17;
改成 uint8 z=table[x]; //其中table=i/17;
7.用乘法代替除法
(替代方法不等价,很多编译器都不会帮你做这种优化)
比如: double x=y/21.3432575;
改成:
double x=y*(1/21.3432575); //如果编译器不能优化掉(1/21.3432575),请预先计算出该结果
对于整数,可以使用与定点数相似的方法来处理倒数
(该替代方法不等价)
比如:
uint32 x,y; x=y/213;
改成: x=y*((1<<16)/213) >> 16;
某些应用中y*((1<<16)/213)可能会超出值域,这时候可以考虑使用int64来扩大值域
uint32
x=((uint64)y)*((1<<31)/213) >> 31;
也可以使用浮点数来扩大值域
uint32 x=(uint32)(y*(1.0/213)); (警告:
浮点数强制类型转换到整数在很多高级语言里都是
一条很慢的操作,在下几篇文章中将给出其优化的方法)
对于这种方法,某些除法是有与之完全等价的优化方法的:
无符号数除以3: uint32
x,y; x=y/3;
推理: y
y y (1<<33)+1 y
x=y/3
=> x=[-] => x=[- + ---------] => x=[--------- * -------] // []表示取整
3 3
3*(1<<33) 3 (1<<33)
y 1
(可证明: 0 <= --------- < - )
3*(1<<33) 3
即: x=(uint64(y)*M)>>33;
其中魔法数M=((1<<33)+1)/3=2863311531=0xAAAAAAAB;
无符号数除以5: uint32 x,y; x=y/5;
(y<(1<<31))
推理: y y
3*y (1<<33)+3 y
x=y/5 =>
x=[-] => x=[- + ---------] => x=[--------- * -------]
5 5
5*(1<<33) 5 (1<<33)
3*y 1
(可证明: 0 <= --------- < -
)
5*(1<<33)
5
即:
x=(uint64(y)*M)>>33;
其中魔法数M=((1<<33)+3)/5=1717986919=0x66666667;
无符号数除以7: uint32 x,y; x=y/7;
推理
:略
即:x=((uint64(y)*M)>>33+y)>>3;
其中魔法数M=((1<<35)+3)/7-(1<<32)=613566757=0x24924925;
对于这种完全等价的替代,还有其他替代公式不再讨论,对于有符号除法的替代算法没有讨论,很多数都有完全等价的替代算法,那些魔法数也是有规律可循的;有“进取心”的编译器应该帮助用户处理掉这个优化(vc6中就已经见到! 偷懒的办法是直接看vc6生成的汇编码;
8.对于已知被除数是除数的整数倍数的除法,能够得到替代算法;或改进的算法;
这里只讨论除数是常数的情况;
比如对于(32位除法):x=y/a; //已知y是a的倍数,并假设a是奇数
(如果a是偶数,先转化为a=a0*(1<<k); y/a==(y/a0)>>k;a0为奇数)
求得a',使 (a'*a)
% (1<<32) = 1;
那么: x=y/a => x=(y/a)*((a*a') %(1<<32))
=> x=(y*a') % (1<<32); //这里并不需要实际做一个求余指令
(该算法可以同时支持有符号和无符号除法)
比如 uint32 y;
? uint32 x=y/7; //已知y是7的倍数
改成 uint32 x=(uint32)(y*M);
//其中M=(5*(1<<32)+1)/7
9.近似计算除法 (该替代方法不等价)
优化除数255的运算(257也可以,或者1023,1025等等)(1026也可以,推导出的公式略有不同)
比如颜色处理中 : uint8
color=colorsum/255;
改成: uint8 color=colorsum/256; 也就是
color=colorsum>>8;
这个误差在颜色处理中很多时候都是可以接受的
如果要减小误差可以改成:
uint8 color=(colorsum+(colorsum>>8))>>8;
推导:
x/255=(x+x/255)/(255+1)=(x+A)>>8; A=x/255;
把A改成A=x>>8
(引入小的误差);带入公式就得到了: x/255约等于(x+(x>>8))>>8的公式
同理可以有x/255约等于(x+(x>>8)+(x>>16))>>8等其它更精确的公式(请推导出误差项已确定是否精度足够)
10. 牛顿迭代法实现除法
(很多CPU的内部除法指令就是用该方法或类似的方法实现的)
假设有一个函数y=f(x);
求0=f(x)时,x的值;(这里不讨论有多个解的情况或逃逸或陷入死循环或陷入混沌状态的问题)
(参考图片)
求函数的导函数为 y=f'(x); //可以这样来理解这个函数的意思:x点处函数y=f(x)的斜率;
a.取一个合适的x初始值x0; 并得到y0=f(x0);
b.过(x0,y0)作一条斜率为f'(x0)的直线,与x轴交于x1;
c.然后用得到的x1作为初始值,进行迭代;
当进行足够多次的迭代以后,认为x1将会非常接近于方程0=f(x)的解,这就是牛顿迭代;
把上面的过程化简,得到牛顿迭代公式:
x(n+1)=x(n)-y(x(n))/y'(x(n))
这里给出利用牛顿迭代公式求倒数的方法; (用倒数得到除法: y =
x/a = x* (1/a) )
求1/a,
令a=1/x; 有方程 y=a-1/x;
求导得y'=1/x^2;
代入牛顿迭代方程 x(n+1)=x(n)-y(x(n))/y'(x(n));
有迭代式
x_next=(2-a*x)*x; //可证明:该公式为2阶收敛公式; 也就是说计算出的解的有效精度成倍增长;
证明收敛性:令x=(1/a)+dx; //dx为一个很小的量
则有x_next-(1/a)=(2-a*(1/a+dx))*(1/a+dx)-1/a
=(-a)*dx^2
//^表示指数运算符
证毕.
程序可以用该方法来实现除法,并按照自己的精度要求来决定迭代次数;
(对于迭代初始值,可以使用查表法来得到,或者利用IEEE浮点格式得到一个近似计算的表达式;在SSE指令集中有一条RCPPS(RCPSS)指令也可以用来求倒数的近似值;有了初始值就可以利用上面的迭代公式得到更精确的结果)
附录: 用牛顿迭代法来实现开方运算
//开方运算可以表示为
y=x^0.5=1/(1/x^0.5); 先求1/x^0.5
求1/a^0.5,
令a=1/x^2;
有方程y=a-1/x^2;
求导得y'=2/x^3;
代入牛顿方程
x(n+1)=x(n)-y(x(n))/y'(x(n));
有迭代式 x_next=(3-a*x*x)*x*0.5;
//可证明:该公式为2阶收敛公式 //方法同上 证明过程略
HouSisong@GMail.com
tag: PI,超高精度浮点数,TLargeFloat,FFT乘法,二分乘法,牛顿迭代法,borwein四次迭代,AGM二次迭代,源代码导读
摘要: 很多人可能都想自己写一个能够执行任意精度计算的浮点数;我写的第一个程序就是用qbasic计算自然数e到100万位(后来计算PI); 我的blog文章《自己动手打造“超高精度浮点数类”》里有一个C++类的实现TLargeFloat,它能够执行高精度的浮点数运算;演示代码里面有一个计算PI的Borwein四次迭代式和一个AGM二次迭代式(我用它计算出了上亿位的PI小数位:) 本文章是对其源代码的进一步解读;
本系列文章的由来: 源于一次在abp论坛(现在的www.cpper.com)的帖子,那是2004年的时候,有初学者询问高精度计算的原理;我那时比较激动(哈哈),因为这不就是我刚学编程的时候做的吗?!
就计划"重操旧业"用C++给初学者写一个高精度计算的Demo程序(决定实际动手写要归功于abp的codinggirl);第一个版本实现前后花了10小时;但实际上完成的代码没有真正发布,也缺少源代码的分析和介绍;后来放在了csdn的blog上;由于实在太慢,摆在我的blog里“碍眼” ,
最近对它做了一些速度改进;本文章是它的简要导读;
超高精度浮点数类TLargeFloat的完整源代码参见: http://blog.csdn.net/housisong/archive/2005/11/08/525215.aspx
;在导读文章中列出的代码(仅作示例)很可能和实际的代码有区别;导读文章会更关注于算法和原理,而更多的细节需要读者去阅读源代码;
ps:题外话(因为是blog文章,说点题外话才是正常状态),回想自己上大学的时候,不务正业的整天跑(泡)图书馆,对编写计算机程序产生了浓厚的兴趣;我的第一个程序是想计算出自然数e(2.718281828...)的上百万位;由于没有电脑,就把所有代码都写在纸上(用的qbasic语言),觉得差不多了的时候就到学校的计算机房去把代码敲到电脑里;那时候对电脑上编写程序一点概念都没有(高中学习接触的根本不能算写程序);折腾了一阵子后旁边的一个玩游戏中(好像)的学长实在看不下去了,转过头来告诉我怎么打开qbasic环境! 我笨拙的把纸上写好的代码敲入计算机,在修改了几处打字错误后运行成功,运行第一次就正确的输出了e的上千位小数,哈哈,当时很有成就感; 在纸上写程序和修改,并在脑袋中"运行"多遍后再让电脑来实际运行的那段记忆只能是美好的回忆了 (记得有一个程序我甚至写了几十页的)
a.高精度浮点数的表示方法(数据结构)
超高精度浮点数类TLargeFloat的数据结构定义:
class TLargeFloat
{
long m_Sign; //符号位
long
m_Exponent; //保存10为底的指数
std::vector<long> m_Digits;
//小数部分
};
其中: m_Sign用来保存该浮点数的正负号(正1和负1);我用0来代表浮点数0,这样对于某些判断比较方便;
m_Exponent保存的是该浮点数的指数部分,其值代表的是10进制为底的指数 (指数部分可以使用int64整数)
m_Digits数组保存的是该浮点数的小数尾数部分,每个元素保存4位10进制数,也就是取值[0--9999];
对于m_Digits[0],正常情况下的值取值范围为[1000,9999],否则该浮点数就是未规格化的(未规格化小数只存在于运算过程中的临时值);
比如当m_Sign=-1,m_Exponent=-100,m_Digits长度为2,m_Digits[0]==1234,m_Digits[1]=5678;
则这个TLargeFloat代表的是浮点数:-0.12345678E-100
源代码中,为了捕获指数运算超出值域的异常情况我定义了一个TCatchIntError类用来包装m_Exponent所使用的整数类型;TCatchIntError实现了“+=” 、“-=” 、“*=”3个运算符,并处理可能的值域超界,如果发生值域超界将抛出TLargeFloatException类型的异常;
小数部分数据结构的选择,将决定很多算法的具体实现细节;为了容易编码和阅读我舍弃了一些更节约内存的设计也舍弃了一些复杂的优化(比如汇编等);选择4位10进制数将简化很多代码的实现(后面会看到); 对于更专业的实现,建议使用8(或9)位10进制数或者以2进制为基础来实现;
b.其他数到超高精度浮点数类TLargeFloat的转换
TLargeFloat实现了long
double到TLargeFloat的多个转换和相互运算,这样那些能够自动隐式转换到long
double的类型(比如int、float等)也能自动的参与TLargeFloat的运算;程序也提供了TLargeFloat与字符串类型之间的转换方法:AsString()和StrToLargeFloat();
这对于超大和超高精度的数传递给TLargeFloat就比较有用,而且通过字符串的方式转换到TLargeFloat的数也不会产生任何误差;
数值转换成TLargeFloat的构造函数,一般的声明是 TLargeFloat(数值,高精度浮点数的有效精度);但这样写容易混浠,比如 TLargeFloat(200,300); 所以我定义了一个类TDigits,要求这样写代码:TLargeFloat(200,TLargeFloat::TDigits(300));
c.高精度浮点数的加减法的实现
加减法可以通过绝对值加和绝对值减来实现(Abs_Add(),Abs_Sub_Abs()函数),先考虑参与运算的TLargeFloat浮点数的正负号然后调用绝对值加(符号相同)或绝对值减(符号不同)就可以了;
要实现两个高精度数相加减,需要首先调整到指数相同(就如手工计算时的对齐);
比如: (-0.1234E2) + (-0.5678E-1) ==
-(0.1234E2 + 0.5678E-1)== -( 0.1234 +0.0005678 )*1E2 == -0.1239678E2
(-0.1234E2) + (0.5678E-1) == -(0.1234E2 - 0.5678E-1)== -( 0.1234 -0.0005678
)*1E2 == -0.1228322E2
核心实现函数:
小数部分的多位数对齐加法: void
TLargeFloat_ArrayAdd(TInt32bit* result,long rsize,const TInt32bit* y,long
ysize);
它实现将y加到result上;
小数部分的多位数对齐减法: void
TLargeFloat_ArraySub(TInt32bit* result,long rsize,const TInt32bit* y,long
ysize);
它实现从result中减去y;
d.高精度浮点数的乘法的简单实现
回想一下我们是怎么手工计算多位数乘法的,那高精度浮点数的乘法也可以这样实现;
核心实现函数:
小数部分的多位数乘法:
TLargeFloat_ArrayMUL_ExE(TInt32bit* result,long rminsize,const TInt32bit* x,long
xsize,const TInt32bit* y,long ysize)
简要过程如下:
for (long
i=0;i<xsize;++i)
{
for (long j=0;j<ysize;++j)
{
result[j+i+1]+=(x*y[j]);
...//进位
}
}
实际的代码做了一些优化;为了减少进位的次数,当result快超出值域的时候,才会执行进位;
该函数的时间复杂度为O(N*N);在进行高精度运算过程中(N很大的时候),绝大部分时间都在做乘法运算;后面会对该实现进行优化;
e.高精度浮点数的除法和开方的实现 牛顿迭代法
高精度浮点数的除法和开方可以用牛顿迭代法来实现,也就是利用乘法来实现;原理和推导(源代码中也有)可以参见我的blog文章《代码优化-之-优化除法》:http://blog.csdn.net/housisong/archive/2006/08/25/1116423.aspx (包括牛顿迭代法的原理和递推公式的导出和收敛性证明)
由于有效精度随迭代次数成倍增加,所以可以控制当前参与运算的高精度数的精度,从而优化速度,减少不必要的计算;完成该自动精度调整功能的类是TDigitsLengthAutoCtrl;
f.用二分法来优化乘法实现
两个高精度的数相乘x*y ; 将x,y从数组中间分成两部分表示:
x=a*E+b; y=c*E+d;
x*y=(a*E+b)*(c*E+d)=(E*E-E)*ac + E*(a+b)*(c+d)
-(E-1)*bd;
可见,长度为N的数相乘,可以分解为3次长度为N/2的数相乘(递推); 时间上有递推式 f(N)=3f(N/2);
求解该方程可得该算法时间复杂度为(N^log2(3));
比前面的O(N^2)复杂度低了很多;
二分法乘法核心实现函数:void ArrayMUL_Dichotomy(TInt32bit* result,long rminsize,const
TInt32bit* x,const TInt32bit* y,long MulSize);
g.用FFT来优化乘法实现
高精度数的乘法其实可以看作一种卷积运算,可以转化为傅立叶变换运算
(进阶实现:转换成数论变换!) ,而它可以有O(N*log(N))复杂度的算法--快速傅立叶变换(FFT);
FFT核心函数: void FFT(TComplex* a,const TComplex* w,const long n);
FFT乘法核心实现函数:void MulFFT(const TInt32bit* ACoef,const long ASize,const TInt32bit*
BCoef,const long BSize,TInt32bit* CCoef,const long CMinSize);
这里的实现使用的是double实现的复数算法;当乘数的长度超过一定范围的时候,计算中产生的误差会放大到超过0.5从而造成取整错误;
所以MulFFT支持的运算长度是有限制的;采用4个10进制数为基时长度不能超过约17百万位10进制位;当然可以使用更短的基从而提高允许的最大的位数;但实际上这样的内存消耗太恐怖了,所以不适合采用,而是在超过设定长度的时候转调用ArrayMUL_Dichotomy的实现;
这里我们有了3个乘法的实现,它们在不同的情况下各有各的优势,当乘法位数较短的时候,TLargeFloat_ArrayMUL_ExE更快;再大一些时使用ArrayMUL_Dichotomy更快,再大一些的话使用MulFFT,当超过MulFFT允许的最大长度时调用ArrayMUL_Dichotomy来拆开乘法运算,使乘法长度适合MulFFT;
这个综合函数就是:void ArrayMUL(TInt32bit* result,long rminsize,const TInt32bit* x,long
xsize,const TInt32bit* y,long ysize); 它是整个高精度运算库的核心;
h.高精度PI值计算采用的公式
我使用超高精度浮点数类TLargeFloat实现AGM算法计算出了PI的1亿位小数;
AGM二次收敛迭代公式:
初值:a=x=1
b=1/sqrt(2) c=1/4
重复计算:y=a a=(a+b)/2 b=sqrt(by) c=c-x(a-y)^2
x=2x
最后:pi=(a+b)^2/(4c)
实现它的函数是: TLargeFloat GetAGMPI(unsigned long
uiDigitsLength);
我也实现了Borwein四次收敛迭代式:TLargeFloat GetBorweinPI(unsigned
long uiDigitsLength);