`
lijun87
  • 浏览: 263645 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

浮点数精确运算的分析和解决办法

阅读更多

1.01 + 2.01 = 3.02
    2.01 * 2.01 = 4 0401
   不知你注意没有,这个很寻常的等式,你如果将它放在C++中,Java中,Basic中,它
居然是不成立的。计算机在开玩笑吗?噢,对了,隐约记得这好象是浮点数的问题,似乎
很多很多年前,老师说过。还有某位姓林的先生在某本书里提过=0的判断。
   嗯,如果你不遇到此问题,那你完全可以把它抛到火星上去,可惜,偶不好彩,这样的
问题,被俺遇到了。唉!
   why?how?
  
   没办法,硬着头皮,从头开始。

一:为何不成立?Why?
   这得从浮点数的在计算机内的存储开始说起,我这里闲话少说。我们只谈双精度double
数(至于float,基本上是五十步和一百步的区别)。
   双精度数在计算机内的表示方式是:(三部分组成)
   符号(正或负)  阶码(2的N次幂)   尾数(大于等于1小于2的数)
   比如: -(符号) 1.01(尾数) * 2~1(N = 1)  = - 2.02
   具体到计算机的存储单元:双精度数共占8字节(64bit)
   符号位(占1个bit) 阶码(11个bit) 尾数(52个bit)
解释一下:
   符号位:0表示正 1表示负
   阶码:是一个偏移量,1023的偏移量,它的1023相当于0,小于1023时为负,
大于1023时为正,如:10000000001表示指数为1025 - 1023 = 2,表示真值为2^2。

好了,知道了原理,我们开始分析上述等式为何为不等。
(相应数的存储值,可以简单用C语言的指针方式取出)
1.01 表示为:
0   0111111 1111    0000 00101000 11110101 11000010 10001111 01011100 00101001 
2.01 表示为;
0   1000000 0000    0000 00010100 01111010 11100001 01000111 10101110 00010100
3.02 表示为:
0   1000000 0000    1000 00101000 11110101 11000010 10001111 01011100 00101001
2.01+1.01 在编程语言中的计算结果 表示为:
0   1000000 0000    1000 00101000 11110101 11000010 10001111 01011100 00101000
好了,我们可以比较一下3.02和计算结果,果然有所不同,只不过最后一个bit不同嘿。
为了验证一下,可以用手工计算一下2.01+1.01:
先把1.01的幂次变为1(与2.01的阶码相同),于是,将尾数右移一位。得到:
  1000 00010100 01111010 11100001 01000111 10101110 000101001
加上2.01的尾数。
  0000 00010100 01111010 11100001 01000111 10101110 00010100
得到:
  1000 00101000 11110101 11000010 10001111 01011100 00101000
嗯,与计算机的计算结果相同,我们的运算思路是正确的。

因此,结论出来了,因为浮点数在计算机内的存储存在偏差,导致运算时,与实际期望的结
果不同。很多时候,你可以不理它,但是,可以肯定负责任的说,发射卫星的运算时,你
需要知道,否则,卫星一转眼就不见了。

二:不成立的的原因找到了,那怎么解决这个问题呢,How?
一个简单的解决办法是:
不要用浮点数来存储浮点,对于VC,Java,Basic,最好的办法是用Decimal来保存它。
下面是分别的实现:(以加法为例,其它四则运算处理相同)
VC中:
double doublAdd(double dbl1, double dbl2)
{
 double dblResult;
 DECIMAL dec1,dec2,decResult;
 ::VarDecFromR8(dbl1,&dec1);
 ::VarDecFromR8(dbl2,&dec2);
 ::VarDecAdd(&dec1,&dec2,&decResult);
 ::VarR8FromDec(&decResult,&dblResult);
 return dblResult;
}
VB中:
Private Function doubleAdd(ByVal dbl1 As Double, ByVal dbl2 As Double) As Double
    doubleAdd = CDec(dbl1) + CDec(dbl2)
End Function

Java中:
public static double add(double v1, double v2) {
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    BigDecimal b2 = new BigDecimal(Double.toString(v2));
    return b1.add(b2).doubleValue();
}
解决思路就是:用其它精确的表示法来存储浮点数,就这么简单。
注意:VC示例中,VarDecFromR8是做了手脚地,如果能直接用VarDecFromStr那更好。

三:在C/C++中,似乎很不情愿看到类似上例中的代码,因为它看起来很低效,还有其它方法吗?
好象还有,对了,只是好象。
我们再来看看双精度数的表示法:
尾数一共有52个bit,也就是最小能表示的数是 2^-52,取对数可得出,约是
在小数点后16位,那也就是说小数点后15位是可以精确表示的,加上前置的默认1,一共有16位
数字是精确可靠的。
我们来试验一下,看上述结论是否成立。
看看VC调试器的显示值。
2.01 的显示值: 2.0099999999999998
如果只取16位有效数字,那么将最后一位8四舍五入,我们得到正确的表示。
好了,这能说明什么呢?

四:我们先看比较简单的加,减法运算。
对于加法:dbl1 + dbl2:
假设dbl1=1.01 那么,16减去整数位1,我们可以假定,在计算机表示中:
小数点后的15位都是精确的。
假设dbl2=100.01 那么 16-3,假定小数点后13位是精确的。
凭经验我们可以知道,两个小数相加,小数点后的精度不会大于精度销大的一个。
所以,我们判定得出结果的精确度可以用较大的一个为准。
于是,将得出的结果,去掉不精确的位数,则应该可以得到准确值。
VC实现如下:
#define DELTA_RATE  16
int getRound(double dbl)
{
 COleVariant var(dbl);
 COleVariant varForLog(dbl);
 ::VarRound(&varForLog,0,&varForLog);
 int nIntCount = log10(varForLog.dblVal>0?varForLog.dblVal:-varForLog.dblVal) + 1;
 int nRound = DELTA_RATE - nIntCount;
 return nRound;
}
double doublAdd2(double dbl1, double dbl2)
{
 COleVariant var(dbl1+dbl2);
 int r1 = getRound(dbl1);
 int r2 = getRound(dbl2);
 ::VarRound(&var,max(r1,r2),&var);
 return var.dblVal;
}
做过一些实验,好象是正确的。同理可以实现doubleSub2的函数。
注意:这里并不用下面五所提的取精度的方式,因为取精度的运算更低效。

五:对于乘除法呢?问题有些复杂,先找出一个需要处理的例子。
如:2.01*2.01=4.0401。
试了一下,不成立。
用方法一的Decimal方式测试,可以通过。
那么方法二呢?
再做假设吧,假设dbl1有两位小数,dbl2也有两位小数,按理论,
可得出相乘后,最大可能是2+2位小数。那么,我们按照 4位小数
进行Round处理,可能会得出正确的结果。
实际上,要取一个双精度的10进制表达的小数位,我没有找到什么好办法,
我能想到的:也就是将数字转为字串,然后查找.后的位数。这样,显然是
非常低效的,这里,我就不再写出代码了。

六:比较方法一和方法二。方法二并不高效,并且还有一些不定因素,所以,
最好采用方法一来统一处理浮点数的运算。
至于效率,实际上最佳方法是从程序的设计着手,将double从程序中去除掉。
比如在VC中,可以用Variant::Decimal来彻底替换double,这样,就不存在
中间的转换了,效率自然就提高了。有关Decimal的常用函数是:
VarDecFromStr VarDecAdd VarDecSub VarDecMul VarDecDiv ……
VarBstrFromDec
至于Java和VB,也可以方便的找到相应函数。

很想找到一种更好的方法,总觉得用Decimal来进行运算很不爽,但真的没找到?
其实呢,做了一下测试,Decimal的运算并不慢,如果可以将内部存储改为Decimal,
那就可以彻底解决问题了。

分享到:
评论

相关推荐

    js 浮点数加法运算

    javascript浮点数加法运算精确计算方法,能够有效避免无限循环小数的产生

    C语言浮点数运算

    有些C语言书上说float型的有效位数是6~7位,为什么不是6位或者7位?而是一个变化的6~7位? 浮点数在内存中是如何存放的? float浮点数要比同为4... 如何才能精确比较浮点数真实的大小? 看完本文档,你将会得到答案!

    简单谈谈php浮点数精确运算

    如果用php的+-*/计算浮点数的时候,可能会遇到一些计算结果错误的问题,所以基本上大部分语言都提供了精准计算的类库或函数库,比如php有BC高精确度函数库,下面我们介绍一下一些常用的BC高精确度函数使用。

    浮点数减法运算

    linux下实现的浮点数精确计算,可以实现小数位借位

    python中实现精确的浮点数运算详解

    计算机智能处理可数集合的运算,但是全体实数是不可数的,所以计算机只能用一些...下面这篇文章主要给大家介绍了关于python中实现精确浮点数运算的相关资料,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧。

    使用Vue.js及Element编写的精度浮点运算计算器源码(打开运行)

    由于javascript的浮点运算是不精确的(对于大多数编程语言都是这样),此程序使用整数来代替浮点数进行运算,最后再转化为浮点数,可以实现对设置范围内(运算范围在程序中为自定义变量range)的浮点运算实现精度...

    提供精 确的浮点数运算,包括加减乘除和四舍五入

    由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精 确的浮点数运算,包括加减乘除和四舍五入。

    C++实现浮点数精确加法

    本文实例为大家分享了C++实现浮点数精确加法的具体代码,供大家参考,具体内容如下 实现两个正浮点数的精确加法 参与运算的浮点数及计算结果所需存储空间都不会超过main函数中的定义 main函数框架要求如下: int ...

    解决js浮点数运算的精度问题

    在javascript中进行运算的时候经常会出现浮点数的问题,导致运算结果不准确 比如:0.1 + 0.2 = 0.30000000000000004 完整demo及解决方案如下: demo .num-tags, .total-tag { text-align: center; } i { ...

    JS浮点数运算结果不精确的Bug解决

    主要给大家介绍了关于JS浮点数运算结果不精确的Bug解决,文中通过示例代码介绍的非常详细,对大家学习或者使用JS具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

    java精确计算浮点数工具类

    Java对浮点数的计算是不精确的,比如0.05+0.01结果不是0.06,而是0.060000000000000005,更有甚者,一个数除以0.0,Java是不会抛异常,而是得出无穷大的结果.本工具类解决了上述问题.该类提供了加减乘除四则运算的精确计算...

    Python如何执行精确的浮点数运算

    主要介绍了Python如何执行精确的浮点数运算,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下

    Java中实现浮点数的精确计算

    Java中的简单浮点数类型float和double不能够进行运算。不光是Java,在其它很多编程语言中也有这样的问题。在大多数情况下,计算的结果是准确的,但是多试几次(可以做一个循环)就可以试出类似上面的错误。

    非精确浮点数乘法器设计

    随着电路系统数值运算范围以及数据运算精度的不断扩大,浮点数运算的研究变得越来越重要。但传统浮点数运算单元硬件复杂、功耗大、延时长,这些因素很大程度上制约着浮点数运算的性能。非精确计算可以减少容错设备的...

    java大数乘法的简单实现 浮点数乘法运算

    大数乘法可以进行任意大小和精度的整数和浮点数的乘法运算, 精确度很高, 可以用作经融等领域的计算,这个是我看了一些资料, 然后自己整理实现的,简单测试了一下

    关于Python中浮点数精度处理的技巧总结

    而python是以双精度(64)位来保存浮点数,多余的位会被截掉,所以看到的是0.1,但在电脑上实际保存的已不是精确的0.1,参与运算后,也就有可能点误差,特别是金融邻域里面,对精度更是要求更高,如何在Python中获取...

Global site tag (gtag.js) - Google Analytics