浮点型 float 计算的精度问题(未完成)

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位的二进制数。

image.png

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 存储:


image.png

我们来举个例子看看:
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

image.png

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 问题解决

# 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.
image.png

对比几组结果:

# 它们相等吗?
>> 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)
分析:

  1. 第一步,将168除以2,商84,余数为0。
  2. 第二步,将商84除以2,商42余数为0。
  3. 第三步,将商42除以2,商21余数为0。
  4. 第四步,将商21除以2,商10余数为1。
  5. 第五步,将商10除以2,商5余数为0。
  6. 第六步,将商5除以2,商2余数为1。
  7. 第七步,将商2除以2,商1余数为0。
  8. 第八步,将商1除以2,商0余数为1。
  9. 第九步,读数,因为最后一位是经过多次除以2才得到的,因此它是最高位,读数字从最后的余数向前读,即10101000

B) 小数部分

方法:乘2取整法,即将小数部分乘以2,然后取整数部分,剩下的小数部分继续乘以2,然后取整数部分,剩下的小数部分又乘以2,一直取到小数部分
为零为止。如果永远不能为零,就同十进制数的四舍五入一样,按照要求保留多少位小数时,就根据后面一位是0还是1,取舍,如果是零,舍掉,如果是1,向入一位。换句话说就是0舍1入。读数要从前面的整数读到后面的整数,下面举例:
例1:将0.125换算为二进制
得出结果:将0.125换算为二进制(0.001)
分析:

  1. 第一步,将0.125乘以2,得0.25,则整数部分为0,小数部分为0.25
  2. 第二步, 将小数部分0.25乘以2,得0.5,则整数部分为0,小数部分为0.5
  3. 第三步, 将小数部分0.5乘以2,得1.0,则整数部分为1,小数部分为0.0
  4. 第四步,读数,从第一位读起,读到最后一位,即为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. 参考文献

忘记了,如果有引用的,可以麻烦通知一下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容