https://github.com/camsong/blog/issues/9
1. 计算案例
使用浮点数进行计算,即使很简单的加运算,都会有可能发生精度丢失的问题。
1.1 案例1
在 java 中计算:
double d = 0.2 + 0.4
结果是 0.6000000000000001
1.2 案例2
在 js 中计算:
0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1
结果是:0.8999999999999999
1.3 案例3
0.21111111 * 100
结果是:21.111110999999998
[40] pry(main)> 0.6.to_d.to_f
=> 0.599999999999999 # 并不会肯定出现(感觉是 0.6 本身的问题,当时的 0.6 并非精确表示,后面几个地方也出现过类似问题)
[45] pry(main)> BigDecimal.new(0.6.to_s).to_f
=> 0.6
https://apidock.com/ruby/Float/to_d
惊讶这这么简单的浮点数的计算案例,都产生误差。而且绝大部分开发语言都会这样,这是为什么?这就需要了解的是浮点数在计算机中的存储标准。
2. 精度丢失分析
计算机处理器的浮点数使用的是IEEE 754 二进制浮点数算术标准。该标准定义了浮点型的计算、异常 和 rounding 等等。
在计算机中,浮点数是以二进制数进行表示,并且通过有限的符号位、指数位 和 尾数位等构成(科学计数法)。
==造成浮点数运算的误差主要有以下原因:==
- 二进制无法表达很多浮点数(除不尽)
- 在 IEEE 754 中,浮点数的指数与尾数位有限,所以导致了更多的浮点数无法精确表示。
举例:IEEE 754 浮点数的尾数位为23位,如果有个浮点数需要25位才能精确表达,那么就会产生了精度错误的问题)
下面,我们将对此进行详细讲述。
有关于 IEEE 与 IEEE 754,可以点击进入:
https://www.ieee.org/
http://cn.ieee.org/about_IEEE.html
https://en.wikipedia.org/wiki/IEEE_754
2.1 IEEE 754 浮点数的结构
https://akaedu.github.io/book/ch14s04.html
浮点数的结构组成并不影响浮点数的表达,更不会造成精度丢失,主要原因是该结构的每一部分都是有长度限制的,有可能无法完整表达。
浮点数的结构
浮点数按以下三部分组成:
- 符号位 (Sign): 0代表正,1代表为负
- 指数位(Exponent): 用于存储==科学计数法中的指数==数据,并且==采用移位存储==
- 尾数部分(Mantissa): 尾数部分
科学计数法
对指数位 和 尾数位的不理解?
在计算机中,浮点数的表示是基于科学计数法(Scientific Notation),举例子:
32767 数字的科学计数写成 3.2767e+4,其中 3.2767 称为 ==尾数==(Mantissa,或者叫Significand),4 称为 ==指数==(Exponent)。
浮点数在计算机中的表示与此类似,只不过基数(Radix)是 2 而不是10。
单双精度浮点数
下述我们将通过图示清晰表达出浮点数的构成。
IEEE754标准是一种浮点数表示标准,一般分为单 (float)、双精度 (double) 两种。单精度是32位的二进制数,双精度是64位的二进制数。
2.2 浮点数与二进制
数值数据是表示数量大小的数据,有多种表示方法。日常生活中一般采用十进制数进行计数和计算,但十进制数难以在计算机内直接存储与运算。==在计算机系统中,通常将十进制数作为人机交互的媒介,而数据则以二进制数的形式存储和运算==。
==在存储为 float 或者 double 类型数据前,我们需要先将 看到的 十进制 浮点型转化为 二进制,然后再根据类型的定义,进行转化存储。==
案例1 中我们看到的 0.2 和 0.4 都是浮点数的十进制表达式,实际计算机中,它是 按 IEEE 754 标准进行存储的,以 0.2 为例,假设它是单精度类型,那么它真实就是长度为 32位的 1/0 组成的数字,想知道真实的数字,我们可以通过以下的操作:
- 十进制 转化为 二进制
- 二进制的科学计数法(用幂的形式)
- 按 IEEE 754 存储
A) 二进制 (binary)
字节(byte)是计算机中数据处理的基本单位,一个字节由8位二进制组成。所以我们要理解浮点数,先要把我们看到的十进制浮点数转化为实质上它的二进制表示。
二进制数据也是采用位置计数法,其位权是以2为底的幂。
例如:
二进制数据101,逢2进1,其权的大小顺序为2²、2¹、2º(后面统一表示为 2^2、 2^1、 2^0)
二进制数据101.11,其权的大小顺序为 2^2、 2^1、 2^0、 2^-1、 2^-2,它的加权系数形式 (转化为10进制就很好理解了):
101.11 (二进制)
= 1 x 2^2 + 0 x 2^1 + 1 x 2^0 + 1 x 2^-1 + 1 x 2^-2
= 4 + 1 + 0.5 + 0.25
= 5.75 (十进制)
注意上面中对于小数点的转化,a 的 小数幂 = a 的正数幂的倒数。
以十进制为例:
0.234 (十进制)
= 2 x 1/10进制正数位第一位 + 3 x 1/正数位第二位 + 4 x 1/正数位第三位
= 2 x 1/10^1 + 3 x 1/10^2 + 4 x 1/10^3
= 2/10 + 3/100 + 4/1000
所以
0.1(二进制)
= 1 X 2^-1
= 1 x 1/(2^1)
= 0.5 (十进制)
详见:https://www.zhihu.com/question/20610504。
这里也可以得出,二进制的负数是由以下十进制加权(1 或 0)组成:0.5, 0.25, 0.125, 0.0625.... 组成。
B) 十进制(decimal)转 二进制 案例
下面,我们将通过十进制的转化案例,我们主要关注小数位部分的计算。通过观察可以看得出,==如果某个十进制数的小数位通过无限乘与2,都无法转化为整数,或者需要乘与2的次数过多,那么就会出现精度丢失==。
++十进制整数部分转化二进制++
13 (十进制)
= 8 + 4 + 1
= 2^3 + 2^2 + 2^0
= (加权表示) 1 x 2^3 + 1 x 2^2 + 0 x 2^1 + 1 x 2^0
= 1101 (二进制)
也就是 ==除 2 取余数部分倒序==
第1步. 13 / 2 = 6 余 1
第2步. 6 / 2 = 3 余 0
第3步. 3 / 2 = 1 余 1
第4步. 1 / 2 = 0 余 1
从刚才所述知道,十进制可以由2进制进行2的幂加权表示,也就是说....
++十进制的小数部分转化为二进制++
例子0.125 (十进制的小数)
= 0 x 2^-1 + 0 x 2^-2 + 1 x 2^-3
= 0 x 1/2 + 0 x 1/4 + 1 x 1/8
= 0.001 (二进制小数)
也就是 ==剩 2 取整数部分正序==
第1步. 0.125 * 2 = 0.25 整数部分为 0
第2步. 0.25 * 2 = 0.5 整数部分为 0
第3步. 0.5 * 2 = 1 整数部分为 1
第4步. 0.001 (二进制的小数)
例子0.2 (十进制的小数)
第1步. 0.2 * 2 = 0.4 整数部分为 0
第2步. 0.4 * 2 = 0.8 整数部分为 0
第3步. 0.8 * 2 = 1.6 整数部分为 1(进位)
第4步. 0.6 * 2 = 1.2 整数部分为 1(进位)
第5步. 0.2 * 2 = 0.4 整数部分为 0(重复第1步)
第6步. 0.4 * 2 = 0.8 整数部分为 0(重复第2步)
... (==我们惊讶的发现0.2这么简单的十进制小数,居然2进制是无法完整表达的==)
第N步. 0.0011001100110011.... (二进制的小数)
==第二个例子,很好地解释了二进制进度丢失的原因之一。==
更多计算方法:https://www.cnblogs.com/xkfz007/articles/2590472.html
2.3 浮点数的存储
浮点型 53.125 的 IEEE 754 存储:
我们来举个例子看看:
https://www.rapidtables.com/convert/number/decimal-to-binary.html
浮点数 19.1 的
- 十进制科学计数为:1.91e+1
- 二进制科学计数为:10011.0001100110011001101
IEEE754 转化器:https://www.h-schmidt.net/FloatConverter/IEEE754.html
十进制转二进制:https://www.rapidtables.com/convert/number/decimal-to-binary.html
https://www.exploringbinary.com/floating-point-converter/
转化理论:
http://cstl-csm.semo.edu/xzhang/Class%20Folder/CS280/Workbook_HTML/FLOATING_tut.htm
http://sandbox.mc.edu/~bennet/cs110/flt/dtof.html
https://blog.penjee.com/binary-numbers-floating-point-conversion/
https://www.doc.ic.ac.uk/~eedwards/compsys/float/
在计算中表示为:
- 符号位:0 (正数)
- 指数位:1.91
- 尾数:
2.3 单精度和双精度
https://msdn.microsoft.com/zh-cn/library/hd7199ke.aspx
有固定的地址空间
https://blog.csdn.net/whzhaochao/article/details/12885875
2.4 10进制转2进制
https://blog.csdn.net/linux12121/article/details/51379389
https://blog.csdn.net/crjmail/article/details/79723051
单精度十进制转化为二进制,例子:
数字float 9.125在十进制中用科学计算的方式表示为9.125*10^0 ,但是在计算机中,计算机只认识0和1,所以在计算机中是按照科学计算的二进制的方式表示的:
- 9的二进制表示为1001
- 0.125的二进制表示为0.001
- 所以91.25的表示成1001.001 将其表示成二进制的科学计数方式为 1.001001*2^3
https://blog.csdn.net/hzw05103020/article/details/50626076
https://blog.csdn.net/K346K346/article/details/50487127
视频:
https://www.youtube.com/watch?v=RMWY3hxTW9M
指数部分是使用位移存储。
正数
1.0 => 0 01111111 00000000000000000000000
单精度浮点数
十进制 | 符号位(1) | 指数位(8) | 尾数位(24) |
---|---|---|---|
1.0 | 正 | 2的0次方 | 1.0 |
- | 0 | 127 | 0 |
- | 0 | 01111111 | 00000000000000000000000 |
9.125 | 正 | 2的3次方 | 1.140625 |
- | 0 | 130 | 1179648 |
- | 0 | 10000010 | 00100100000000000000000 |
3. 浮点数的计算问题
返回刚才的案例
3.1 精度计算
我们都知道,使用float会导致精度丢失,为什么会丢失?
有限位表示的十进制浮点数
分数 | 十进制 | 二进制 |
---|---|---|
0/0 | 0.0 | 0.0 |
1/2 | 0.5 | 0.1 |
1/4 | 0.25 | 0.01 |
1/8 | 0.125 | 0.001 |
1/16 | 0.0625 | 0.0001 |
从上面的例子就可以看到,当十进制的浮点数不能被有限位(32位/64位或者更长,都是有限位的)的二进制表示时,就产生了精度丢失。
例子中的,0.1是可以被表示,但是 0.1 的二进制 与 0.1 的二进制相加,我们来看看。
3.2 问题解决
-
Decimal (小数的;十进位的)
转化为整数。如:金额保存到分存储
拿刚才的例子:
# ruby
(1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1).to_f/10**9
# = 9.0e-09
注意: ruby 的 to_d 有可能会导致转化问题 (并非 100% 出现),原因是:
# http://cn.voidcc.com/question/p-mtgmemgj-sp.html
def to_d(precision=nil)
BigDecimal(self, precision || Float::DIG)
end
Float::DIG
#=> 15
64.4.to_d 相当于 BigDecimal(64.4, Float::DIG)
to_d 并不等价 BigDecimal,建议使用 BigDecimal,为什么呢?因为它会自动检测float 的小数位,并且保留该小数位
# BigDecimal 的类注释
# new(initial, digits)
#
# Create a new BigDecimal object.
#
# initial:: The initial value, as an Integer, a Float, a Rational,
# a BigDecimal, or a String.
#
# If it is a String, spaces are ignored and unrecognized characters
# terminate the value.
#
# digits:: The number of significant digits, as a Fixnum. If omitted or 0,
# the number of significant digits is determined from the initial
# value.
对比几组结果:
# 它们相等吗?
>> 0.1 + 0.05 == 0.15
=> false
# 来看看 左侧的结果
>> 0.1 + 0.05
=> 0.15000000000000002
# 实际上面的就是真实精度?
>> sprintf("%0.50f", 0.10 + 0.05)
=> "0.15000000000000002220446049250313080847263336181641"
>> sprintf("%0.50f", 0.10)
=> "0.10000000000000000555111512312578270211815834045410"
>> sprintf("%0.50f", 0.05)
=> "0.05000000000000000277555756156289135105907917022705"
# 再来观察几组,可以看到 0.26 是无法通过有限位浮点数表达:
# https://ieftimov.com/post/testing-floats-in-ruby/
>> sprintf("%0.50f", 0.5)
=> "0.50000000000000000000000000000000000000000000000000"
>> sprintf("%0.50f", 0.25)
=> "0.25000000000000000000000000000000000000000000000000"
>> sprintf("%0.50f", 0.26)
=> "0.26000000000000000888178419700125232338905334472656"
3.3 Javascript 的浮点数计算问题
因为弱类型,无法转化为十进制
3.4 十进制的数据库存储问题
2.2 十进制浮点数的二进制表示法
2.2.1 十进制转换为二进制,分为整数部分和小数部分
A) 整数部分
方法:除2取余法,即每次将整数部分除以2,余数为该位权上的数,而商继续除以2,余数又为上一个位权上的数,这个步骤一直持续下去,直到商为0为止,最后读数时候,从最后一个余数读起,一直到最前面的一个余数。下面举例:
例:将十进制的168转换为二进制
得出结果 将十进制的168转换为二进制,(10101000)
分析:
- 第一步,将168除以2,商84,余数为0。
- 第二步,将商84除以2,商42余数为0。
- 第三步,将商42除以2,商21余数为0。
- 第四步,将商21除以2,商10余数为1。
- 第五步,将商10除以2,商5余数为0。
- 第六步,将商5除以2,商2余数为1。
- 第七步,将商2除以2,商1余数为0。
- 第八步,将商1除以2,商0余数为1。
- 第九步,读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,即10101000
B) 小数部分
方法:乘2取整法,即将小数部分乘以2,然后取整数部分,剩下的小数部分继续乘以2,然后取整数部分,剩下的小数部分又乘以2,一直取到小数部分
为零为止。如果永远不能为零,就同十进制数的四舍五入一样,按照要求保留多少位小数时,就根据后面一位是0还是1,取舍,如果是零,舍掉,如果是1,向入一位。换句话说就是0舍1入。读数要从前面的整数读到后面的整数,下面举例:
例1:将0.125换算为二进制
得出结果:将0.125换算为二进制(0.001)
分析:
- 第一步,将0.125乘以2,得0.25,则整数部分为0,小数部分为0.25
- 第二步, 将小数部分0.25乘以2,得0.5,则整数部分为0,小数部分为0.5
- 第三步, 将小数部分0.5乘以2,得1.0,则整数部分为1,小数部分为0.0
- 第四步,读数,从第一位读起,读到最后一位,即为0.001
在线转化:
https://www.sojson.com/hexconvert.html
http://www.binaryconvert.com/result_float.html?decimal=048046049
2.2.2 有限位表示的十进制浮点数
分数 | 十进制 | 二进制 |
---|---|---|
0/0 | 0.0 | 0.0 |
1/2 | 0.5 | 0.1 |
1/4 | 0.25 | 0.01 |
1/8 | 0.125 | 0.001 |
1/16 | 0.0625 | 0.0001 |
从上面的例子就可以看到,当十进制的浮点数不能被有限位(32位/64位或者更长,都是有限位的)的二进制表示时,就产生了精度丢失。
代码之谜(五)- 浮点数(谁偷了你的精度?)
http://justjavac.com/codepuzzle/2012/11/11/codepuzzle-float-who-stole-your-accuracy.html
to_d 计算注意问题
> 0.69*2.to_d
=> 0.13799999999999998e1
> 2.to_d*0.69
=> 0.13799999999999998e1
# 虽然最终类型是 bigdecimal,但是实际过程与先转化为 to_d 还是有区别
> (0.69*2.to_d).class
=> BigDecimal
> (0.69*2.to_d).to_f
=> 1.38
> (0.69.to_d*2.to_d).to_f
=> 1.38
4. 参考文献
忘记了,如果有引用的,可以麻烦通知一下