您的位置:知识库 » 编程语言

诡异的精度问题

作者: Daniel Zheng  来源: 博客园  发布时间: 2010-12-26 21:58  阅读: 684 次  推荐: 0   原文链接   [收藏]  

  今天看了园子里一篇博文,链接如下:

  http://www.cnblogs.com/Hi-ILoveFeng/archive/2010/12/21/1913168.html

int a = (int)(19.9 *100); //19.9默认是double类型
int b = (int)(19.9M *100 ); //将19.9转换成decimal类型

Console.WriteLine(a);
//输出:1989

Console.WriteLine(b);
//输出:1990

  关于decimal的问题引用自己的留言如下:

  二进制无法精确表示浮点数,小数部分一般都是用近似值来标示的。所以19.9*100的二进制标示会有那么多的11111111。而(int)(19.999999999)结果是直接截断小数点后面的结果,所以1989.999999也就是1989了。但是decimal为什么能正确呢,因为它根本就不存在真正的小数存储进制,所有的数都在小数点的右边。那它又是如何存储小数的呢,答案就是比例因子。当然还有符号位。

  我先看IEEE754对double类型在计算机内存中表示的标准:

符号位(1位) 指数位(11位) 尾数(52位)
0表示+,1表示- 偏移值011 1111 1111 小数的右边部分,因为左边永远为1

  举个例子19.9

  符号位0,19.9转换为二进制位10011.1 1100110011001100········也就是1.00111 1100110011001100·······乘以2的4次,所以偏移值位011 1111 1111+100=100 0000 0011,尾数为00111 1100 1100 1100 1100········

double a=19.9在内存中的表示就是:

01000000 00110011 11100110 01100110 01100110 01100110 01100110 01100110

  循环节很简单,0.9*2=1.8=>1, 0.8*2=1.6=>1,0.6*2=1.2=>1,0.2*2=0.4=>0,0.4*2=0.8,0.8*2=1.6=>1

  注意,循环开始了,就是1100。

  大家注意19.9二进制形式的最后一位0,看似很正常对吧。

  如果换成double a=0.1呢,二进制标示为00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010

  最后四个数不是循环节,这里原本是1001(1001 1001 ······),舍去的部分大于最后一位的一半,也就是最近舍入原则,变成1001+1=1010。(关于最近舍入就是对于x+0.5,x是整数,如果x+1是偶数,那么近似为x+1,否则为x.

  其他的与四舍五入没什么区别。)

  小心,这是一个陷阱!也就是说a不是精确等于0.1,而且看上去比准确的0.1大那么一点,那么我们

double a=0;
int count = 0;
for(;a<100;a+=0.1) count++;

  count最后值为多少?应该是从0到99.9,共1000次,a最后的结果为100,结果呢?count为1001,a最后的值为100.099999999999,按理说1001*0.1=100.1。那么应该是0.1在计算机中的值实际小于0.1的准确值才对啊。所以看来不是每次都是用00111111 10111001 10011001 10011001 10011001 10011001 10011001 10011010这个数在相加。对吗?

  我们想想计算机是怎么样计算浮点数加法的?如下:

  第一步:求阶差的绝对值。

  第二步:对阶。注意,精度丢失开始了。阶码小的向大的看齐,丢掉末尾的几位。几位呢?“阶差”位。

  第三步:尾数相加。

  第四步:规格化。

  第五版:舍入计算。再次丢失精度。(不知表达是否最缺,从0.1到0.99再到0.999是丢失精度还是找回精度?大家知道就好。)

  我们来看看刚才的代码,

for(;a<100;a+=0.1) count++;

  明了!a不断变大,而0.1在对阶的时候不断丢失末尾几位数,而且随之a越来越大,丢失的位数越来越多。所以在整个迭代过程中,我们这个所谓0.1的实际平均值是小于0.1的。

  OK, 加法明了了,乘法呢?比如19.9*100?

  现代CPU可不是把乘法转换为加法来计算的,而是有专用的乘法器,乘法器内部则是把乘数与被乘数预处理后计算各个部分积,再相加得到最终结果。(也许已经落伍了)for(int i=1;i<10000;i++) 明显比i*10000慢。

我们来看看19.9*100.0与1990.0在内存中的形式:

Capture

  这可不是我直接算的,直接用char*获取到的。这就是(int)(19.9*100) 结果为1989的原因。但是(19.9*100)与1990.0为什么内存表示不同呢?答案只能是做加法时丢了精度,如何丢的?为什么偏偏是19.9?换一个9.9

Capture

  那么让我们来找一找这样的数,这里我以0.01为步长,0为基数,找50以内的这样的数,let's go!

Capture

  大家看看,天哪,so many landmines.

代码如下:

static void Main(string[] args)
{
for (decimal i = 0; i < 50; i += 0.01M)
{
double j = (double)i;
if (System.Math.Abs(j * 100 - (int)(j * 100)) > 0.5)
{
Console.Write(
"{0,-8}",j);
}
}

  Ok, 还没分析完,今天就到这了。接下来时分析他们的共性,请看下回分解。

0
0
标签:编程 精度

编程语言热门文章

    编程语言最新文章

      最新新闻

        热门新闻