然后就在做第二个实验结尾后意外地发现了MySQL数据类型中float的一个问题,现在帖出来请大家指点。百度中许多同仁也遇到了这个问题--传说中精典的浮点数精度问题。
原文如下:
一、浮点数的概念及误差问题:
浮点数是用来表示实数的一种方法,它用 M(尾数) * B( 基数)的E(指数)次方来表示实数,相对于定点数来说,在长度一定的情况下,具有表示数据范围大的特点。但同时也存在误差问题,这就是著名的浮点数精度问题!
浮点数有多种实现方法,计算机中浮点数的实现大都遵从 IEEE754 标准,IEEE754 规定了单精度浮点数和双精度浮点数两种规格,单精度浮点数用4字节(32bit)表示浮点数,格式是:
1位符号位 8位表示指数 23位表示尾数
双精度浮点数8字节(64bit)表示实数,格式是:
1位符号位 11位表示指数 52位表示尾数
同时,IEEE754标准还对尾数的格式做了规范:d.dddddd...,小数点左面只有1位且不能为零,计算机内部是二进制,因此,尾数小数点左面部分总是1。显然,这个1可以省去,以提高尾数的精度。由上可知,单精度浮点数的尾数是用24bit表示的,双精度浮点数的尾数是用53bit表示的,转换成十进制:
2^24 - 1 = 16777215 2^53 - 1 = 9007199254740991
由上可见,IEEE754单精度浮点数的有效数字二进制是24位,按十进制来说,是8位;双精度浮点数的有效数字二进制是53位,按十进制来说,是16 位。显然,如果一个实数的有效数字超过8位,用单精度浮点数来表示的话,就会产生误差!同样,如果一个实数的有效数字超过16位,用双精度浮点数来表示,也会产生误差!对于 1310720000000000000000.66 这个数,有效数字是24位,用单精度或双精度浮点数表示都会产生误差,只是程度不同:
单精度浮点数: 1310720040000000000000.00
双精度浮点数: 1310720000000000000000.00
双精度差了 0.66 ,单精度差了近4万亿!这个结果为什么与翟振兴例子中的差很多呢?原因是翟振兴的测试用表中对字段进行了限制,实际上显示的是mysql溢出后的值,而我这里给出的是计算机中实际的值,如果把测试表字段精度提高到24位或以上,得到的结果就相同了。
以上说明了因长度限制而造成的误差,但这还不是全部!采用IEEE754标准的计算机浮点数,在内部是用二进制表示的,但在将一个十进制数转换为二进制浮点数时,也会造成误差,原因是不是所有的数都能转换成有限长度的二进制数。对于翟振兴测试中用到的 131072.32 这个数,其有效数字是8位,按理应该能用单精度浮点数准确表示,为什么会出现偏差呢?看一下这个数据二进制尾数就明白了
10000000000000000001010001......
显然,其尾数超过了24bit,根据舍入规则,尾数只取 100000000000000000010100,结果就造成翟振兴测试中遇到的“奇怪”现象!131072.68 用单精度浮点数表示变成 131072.69 ,原因与此类似。实际上有效数字小于8位的数,浮点数也不一定能精确表示,7.22这个数的尾数就无法用24bit二进制表示,当然在数据库中测试不会有问题(舍入以后还是7.22),但如果参与一些计算,误差积累后,就可能产生较大的偏差。
二、mysql 和 oracle中的数值类型:
翟振兴发现的问题是不是只有 mysql 存在呢?显然不是,只要是符合IEEE754标准的浮点数实现,都存在相同的问题。
mysql中的数值类型(不包括整型):
IEEE754浮点数: float (单精度) , double 或 real (双精度)
定点数: decimal 或 numeric
oracle中的数值类型:
oracle 浮点数 : number (注意不指定精度)
IEEE754浮点数: BINARY_FLOAT (单精度) , BINARY_DOUBLE (双精度)
FLOAT,FLOAT(n) (ansi要求的数据类型)
定点数: number(p,s)
如果在oracle中,用BINARY_FLOAT等来做测试,结果是一样的。
因此,在数据库中,对于涉及货币或其他精度敏感的数据,应使用定点数来存储,对mysql来说是 decimal,对oracle来说就是number(p,s)。双精度浮点数,对于比较大的数据同样存在问题!
三、编程中也存在浮点数问题:
不光数据库中存在浮点数问题,编程中也同样存在,甚至可以说更值得引起注意!
通过上面的介绍,浮点数的误差问题应该比较清楚了。如果在程序中做复杂的浮点数运算,误差还会进一步放大。因此,在程序设计中,如果用到浮点数,一定要意识到可能产生的误差问题。不仅如此,浮点数如果处理不好,还会导致程序BUG!看下面的语句:
if (x != y) { z = 1 / (x -y)}
这个语句看起来没有问题,但如果是浮点数,就可能存在问题!再看下面的语句会输出什么结果:
public class Test {
public static void main(String[] args) throws Exception {
System.out.print("7.22-7.0=" + (7.22f-7.0f))
}
}
我们可能会想当然地认为输出结果应该是 0.22 ,实际结果却是 0.21999979 !
因此,在编程中应尽量避免做浮点数的比较,否则可能会导致一些潜在的问题!
除了这些,还应注意浮点数中的一些特殊值,如 NaN、+0、-0、+无穷、-无穷等,IEEE754虽然对此做了一些约定,但各具体实现、不同的硬件结构,也会有一些差异,如果不注意也会造成错误!
四、总结:
从上面的分析,我们可以得出以下结论:
1、浮点数存在误差问题;
2、对货币等对精度敏感的数据,应该用定点数表示或存储;
3、编程中,如果用到浮点数,要特别注意误差问题,并尽量避免做浮点数比较;
4、要注意浮点数中一些特殊值的处理。
June,浮点数问题,很容易被忽视,可能具有一定的普遍性,也许应该发给其他技术人员,以免再出现这方面的问题。
-----Original Message-----
From: htang [mailto:htang@corp.netease.com]
Sent: Tuesday, September 26, 2006 6:29 PM
To: 翟振兴
Cc: LisaLan关宝军韦连友
Subject: RE: RE: mysql中float的问题
这个问题不是一个Bug,而是浮点数本身存在的局限。原因是计算机对浮点数的表示是 M * 2 的 N 次方,其中M是尾数,N是指数,在此转换过程中存在数据损失,因此浮点数(包括double类型)是不能精确表示所有实数的。出现的问题正是由误差和四舍五入造成的。
-----Original Message-----
From: 翟振兴 [mailto:zxzhai@corp.netease.com]
Sent: Tuesday, September 26, 2006 12:17 PM
To: htang
Cc: LisaLan关宝军韦连友
Subject: Re: RE: mysql中float的问题
Importance: High
老唐,您好!
昨天测试发现,当float数据类型超过131072时候,插入的数据会发现不稳定情况,测试过程如下:
mysql>desc test10
+------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+---------------+------+-----+---------+-------+
| floattest | float(12,2) | YES | | NULL| |
| doubletest | double(12,2) | YES | | NULL| |
| dectest| decimal(12,2) | YES | | NULL| |
+------------+---------------+------+-----+---------+-------+
mysql>insert into test10 values(131071,131071,131071)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
+-----------+------------+-----------+
1 row in set (0.00 sec)
mysql>insert into test10 values(131071.32,131071.32,131071.32)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
+-----------+------------+-----------+
2 rows in set (0.00 sec)
mysql>insert into test10 values(131071.68,131071.68,131071.68)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
| 131071.68 | 131071.68 | 131071.68 |
+-----------+------------+-----------+
3 rows in set (0.01 sec)
mysql>insert into test10 values(131072,131072,131072)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
| 131071.68 | 131071.68 | 131071.68 |
| 131072.00 | 131072.00 | 131072.00 |
+-----------+------------+-----------+
4 rows in set (0.00 sec)
mysql>insert into test10 values(131072.32,131072.32,131072.32)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
| 131071.68 | 131071.68 | 131071.68 |
| 131072.00 | 131072.00 | 131072.00 |
| 131072.31 | 131072.32 | 131072.32 |
+-----------+------------+-----------+
5 rows in set (0.00 sec)
mysql>insert into test10 values(131072.68,131072.68,131072.68)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
| 131071.68 | 131071.68 | 131071.68 |
| 131072.00 | 131072.00 | 131072.00 |
| 131072.31 | 131072.32 | 131072.32 |
| 131072.69 | 131072.68 | 131072.68 |
+-----------+------------+-----------+
6 rows in set (0.00 sec)
mysql>insert into test10 values(131072.66,131072.66,131072.66)
Query OK, 1 row affected (0.00 sec)
mysql>select * from test10
+-----------+------------+-----------+
| floattest | doubletest | dectest |
+-----------+------------+-----------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
| 131071.68 | 131071.68 | 131071.68 |
| 131072.00 | 131072.00 | 131072.00 |
| 131072.31 | 131072.32 | 131072.32 |
| 131072.69 | 131072.68 | 131072.68 |
| 131072.66 | 131072.66 | 131072.66 |
+-----------+------------+-----------+
mysql>insert into test10 values(1310720000000000000000.66,1310720000000000000000.66,1310720000000000000000.66)
Query OK, 1 row affected, 3 warnings (0.00 sec)
mysql>select * from test10
+----------------+---------------+---------------+
| floattest | doubletest| dectest |
+----------------+---------------+---------------+
| 131071.00 | 131071.00 | 131071.00 |
| 131071.32 | 131071.32 | 131071.32 |
| 131071.68 | 131071.68 | 131071.68 |
| 131072.00 | 131072.00 | 131072.00 |
| 131072.31 | 131072.32 | 131072.32 |
| 131072.69 | 131072.68 | 131072.68 |
| 131072.66 | 131072.66 | 131072.66 |
| 10000000000.00 | 9999999999.99 | 9999999999.99 |
+----------------+---------------+---------------+
以上测试说明:
当insert的数据范围在+-131072(65536×2)以内的时候,float数据精度是正确的,但是超出这个范围的数据就不稳定,没有发现有相关的参数设置
建议:将float改成double或者decimal,两者的差别是double是浮点计算,decimal是定点计算,会得到更精确的数据。
语言和C#语言中,对于浮点类型的数据采用单精度类型(float)和双精度类型(double)来存储,float数据占用32bit,double数据占用64bit,我们在声明一个变量float f= 2.25f的时候,是如何分配内存的呢?如果胡乱分配,那世界岂不是乱套了么,其实不论是float还是double在存储方式上都是遵从IEEE的规范的,float遵从的是IEEE R32.24 ,而double 遵从的是R64.53。
无论是单精度还是双精度在存储中都分为三个部分:
·符号位(Sign) : 0代表正,1代表为负
·指数位(Exponent):用于存储科学计数法中的指数数据,并且采用移位存储
·尾数部分(Mantissa):尾数部分
其中float的存储方式如下图所示:
而双精度的存储方式为:
R32.24和R64.53的存储方式都是用科学计数法来存储数据的,比如8.25用十进制的科学计数法表示就为:8.25* ,而120.5可以表示为:1.205* ,这些小学的知识就不用多说了吧。而我们傻蛋计算机根本不认识十进制的数据,他只认识0,1,所以在计算机存储中,首先要将上面的数更改为二进制的科学计数法表示,8.25用二进制表示可表示为1000.01,我靠,不会连这都不会转换吧?那我估计要没辙了。120.5用二进制表示为:1110110.1用二进制的科学计数法表示1000.01可以表示为1.0001 2^3 ,1110110.1可以表示为1.1101101 2^6 ,任何一个数都的科学计数法表示都为1.xxx*
,尾数部分就可以表示为xxxx,第一位都是1嘛,干嘛还要表示呀?可以将小数点前面的1省略,所以23bit的尾数部分,可以表示的精度却变成了24bit,道理就是在这里,那24bit能精确到小数点后几位呢,我们知道9的二进制表示为1001,所以4bit能精确十进制中的1位小数点,24bit就能使float能精确到小数点后6位,而对于指数部分,因为指数可正可负,8位的指数位能表示的指数范围就应该为:-127-128了,所以指数部分的存储采用移位存储,存储的数据为元数据+127,下面就看看8.25和120.5在内存中真正的存储方式。
首先看下8.25,用二进制的科学计数法表示为:1.0001*
2^3
按照上面的存储方式,符号位为:0,表示为正,指数位为:3+127=130 ,位数部分为,故8.25的存储方式如下图所示:
而单精度浮点数120.5的存储方式如下图所示:
那么如果给出内存中一段数据,并且告诉你是单精度存储的话,你如何知道该数据的十进制数值呢?其实就是对上面的反推过程,比如给出如下内存数据:0100001011101101000000000000,首先我们现将该数据分段,0 10000 0101 110 1101 0000 0000 0000 0000,在内存中的存储就为下图所示:
根据我们的计算方式,可以计算出,这样一组数据表示为:1.1101101*2^6
=120.5
而双精度浮点数的存储和单精度的存储大同小异,不同的是指数部分和尾数部分的位数。所以这里不再详细的介绍双精度的存储方式了,只将120.5的最后存储方式图给出,大家可以仔细想想为何是这样子的
下面我就这个基础知识点来解决一个我们的一个疑惑,请看下面一段程序,注意观察输出结果
可能输出的结果让大家疑惑不解,单精度的2.2转换为双精度后,精确到小数点后13位后变为了2.2000000476837,而单精度的2.25转换为双精度后,变为了2.2500000000000,为何2.2在转换后的数值更改了而2.25却没有更改呢?很奇怪吧?其实通过上面关于两种存储结果的介绍,我们已经大概能找到答案。首先我们看看2.25的单精度存储方式,很简单 0 1000 0001 001 0000 0000 0000 0000 0000,而2.25的双精度表示为:0 100 0000 0001 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000,这样2.25在进行强制转换的时候,数值是不会变的,而我们再看看2.2呢,2.2用科学计数法表示应该为:将十进制的小数转换为二进制的小数的方法为将小数 2,取整数部分,所以0.282=0.4,所以二进制小数第一位为0.4的整数部分0,0.4×2=0.8,第二位为0,0.8 2=1.6,第三位为1,0.6×2 = 1.2,第四位为1,0.2*2=0.4,第五位为0,这样永远也不可能乘到=1.0,得到的二进制是一个无限循环的排列 00110011001100110011... ,对于单精度数据来说,尾数只能表示24bit的精度,所以2.2的float存储为:
但是这样存储方式,换算成十进制的值,却不会是2.2的,应为十进制在转换为二进制的时候可能会不准确,如2.2,而double类型的数据也存在同样的问题,所以在浮点数表示中会产生些许的误差,在单精度转换为双精度的时候,也会存在误差的问题,对于能够用二进制表示的十进制数据,如2.25,这个误差就会不存在,所以会出现上面比较奇怪的输出结果。
注:本文在写作过程中,参照了如下资料:
http://www.msdn.net/library/chs/default.asp?url=/library/CHS/vccore/html/_core_why_floating_point_numbers_may_lose_precision.asp
http://blog.csdn.net/ganxingming/archive/2006/12/19/1449526.aspx
https://blog.csdn.net/monokai/article/details/108153010
32.35 = 1.xxxx *2^5
浮点数是表示小数的一种方法。所谓浮点就是小数点的位置不固定,与此相反有定点数,即小数点的位置固定浮点数的实现在各种平台上差异很大,有的处理器有浮点运算单元(FPU,FloatingPointUnit),称为硬浮点(Hardfloat)实现。
整数可以看做是一种特殊的定点数,即小数点在末尾。8086/8088中没有浮点数处理指令,不过从486起,CPU内置了浮点数处理器,可以执行浮点运算。
一般的浮点数有点象科学计数法,包括符号位、指数部分和尾数部分。 有的处理器没有浮点运算单元,只能做整数运算,需要用整数运算来模拟浮点运算,称为软浮点(Softfloat)实现。
扩展推荐:
编程学习过程中可以看得书推荐。《代码大全(第二版)》出自著名IT畅销书作者史蒂夫·迈克康奈尔之手,曾被《软件开发》杂志授予优异产品震撼大奖。
《每个程序员都应该知道的97件事情》( 97 things every programmer should know )对于编程初学者来说这本书都可以算上一个优质的入门书籍。本书提供了丰富的编程实践及理念,提供了大量的实例,并且书的排版格式阅读起来十分简洁方便
《计算机程序设计艺术 (第一卷)》由著名的计算机科学家教授Donald Knuth编著,并得到行业内众多顶尖程序员的一致好评。甚至连比尔盖茨也对这本书赞誉有加。
参考资料:百度百科-浮点数
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)