深入理解计算机系统 - 浮点 - 0.1 + 0.2 不等于 0.3?

深入理解计算机系统 - 浮点 - 0.1 + 0.2 不等于 0.3?

技术杂谈小彩虹2021-07-15 9:14:03110A+A-

0.1 + 0.2 不等于 0.3,我们来看看为什么不等于?我们先来看看浮点的原理

浮点是什么

浮点表示小数点的表现在计算机的表示方式,是一种近似值的表现法

为什么是一种近似值的表演方式?

因为计算机是二进制表示,通过2^-n ,表示小数点的数字,这种方式有些数字不能表示出来

如 0.1 ~= 0.100000001490116119384765625,通过计算机CPU的精度进行减少误差

计算机是怎样表示的

浮点分,单精度与双精度,在64位系统里面区别在于单精度使用32位,双精度使用64位

我们来看看它的结构

S singificand 表示符号位,0表示正数,1表示负数

Exp exponent 表示阶码(E),表示科学计数法的 n,计算机使用二进制 因此使用 2^E, 阶码的意思是指,二进制数的小数点向左移动多少位才剩下 1. 或 0. 为首位的

Frac, 表示用二进制表示小数点后的数, M表示十进制,说明能存储多少位小数,有些无限循环的二进制当位数不够会采用偶数舍入方式进位

单精度,s=1bit, exp = 8bit, frac = 23bit

双精度,s=1bit, exp = 11bit, frac = 52bit

其IEEE 浮点标准表示方式,V=(-1)^s x M x 2^E

v2-2d3b415b1b49afd9a7c00d619dcccc03_b.png

所以10.5 是怎样存储的

我们需要把上面的数拆解为 V=(-1)^s x M x 2^E

10.5 转 二进制,分两部分 10的二进制 = 1010, 0.5 的二进制 0.1 (具体参考十进制转二进制的辗转相乘除法)

因此 10.5的二进制位 1010.01,标准表示方式 1.01001 * 2^3

v2-d1092ccc683222bb5c59a04e9845da51_b.png

关于阶码 E 的解析

e 表示 exp 的二进制,Bias 表示偏移量 = 2^(k-1) - 1, k为exp的位数单精度为8,E= e - Bias 等于阶码

v2-e5368b93b1ef53584193965def6253dd_b.png

k 表示exp的位数,这里表示8,k=8

e 表示exp的二进制,10000010

Bias 表示偏移量,2^(k-1) - 1,这里k=8 Bias等于 127

E 表示阶码科学计数法的幂,E=e-Bias (e需要转10进制计算),

e - Bias = 3 , Bias = 127 求 e = (3 + 127)二进制 = 10000010

普及知识

非规格化,exp全为0,指0为头的表示方式,如0.10101. 0.0

规范化,exp 介于0~全1之间

特殊化,exp全为1,表示无穷大,nan

为什么需要偏移量 Bias

因为要表示阶码的负数,表示更多的范围,如果没有这个Bias ,exp的取值范围[0,255] ,没有负数,想扩展多0.00000的机会都没有,因此需要bias [-128,127] 为什么是这个值,因为是使用补码

浮点舍入

向偶数舍入,将数字向上或向下舍入,使得结果最低有效数字是偶数,如1.4,不到一半,1.4 = 1,-1.5 也变成-2,如果2.5,则变为2,因为偶数舍入是找最近的偶数进行舍入

向零舍入,把整数向下舍入,把负数想上舍入,如 1.4 = 1,1.6 = 1,-1.5 = -1

向下舍入,把整数和负数都向下舍入,1.4 = 1, 1.6 = 1,-1.5 = 2

向上舍入,把正数和负数想上舍入,1.4 = 2,1.6 = 2 , -1.5 = -1

为什么默认采用偶数舍入?

避免了统计偏移,舍入方法把一组数值,计算这些值的平均数中引入统计偏差,采用欧舍舍入得到一组数的平均值比这些数本身的平均值略高,相反向下舍入,比平均值略低

偶数舍入,在50%时向上舍入,50%,向下舍入

如1.234999 舍入 1.23,1.235舍入1.24

因为在1.23和1.24正中间,有 1.235 和 1.245,两者都舍入到。124,因为4是偶数

**有限的精度不是主要问题,计算相对误差才是? **

如何理解?

例如 0.1 在二进制里面是无限循环,0.0 0011 0011[0011] 括号是无限重复

程序可使用24位存储,在我们的系统中,是使用23位

X = 0.00011001100110011001100

当 0.1 - x 的二进制表示,为 0.000000000000000000000001100[1100],23位都是0,后续1100[1100] 是0.1二进制的剩余

0.1 - x 的十进制值是多少呢?

0.1 = 0.000000000000000000000001100[1100]

x = 0.00011001100110011001100

两者相减为

0.000000000000000000000001100[1100]…不断重复[1100]

最终转化为 0.1 x 2^-20 大约为 9.54 x 10^-8

ps:2^-20 次方式怎样来的,0.1 是怎样来的

按照上面的 0.000000000000000000000001100[1100]…不断重复[1100],我们抽取 20个零,为 2^-20 让后剩下 0.0001100[1100] 这个就是0.1 因此等于 0.1 x 2^-20

产生了这些误差,如果在一个运动系统里面进行,相乘100小时,9.54 x 10^-8 x 100 x 60 x 60 x 10 ~= 0.343秒误差

在乘速率 0.343 * v 如 v = 300m/s = 102.9m

这种在航天,导弹,自动驾驶都是至关重要的

假设采用偶数舍入

0.1 ~= 0.00011001100110011001100 采用偶数方式23位表示, x` = 0.00011001100110011001101

最后的0变为1的原因 0.1 ~=0.00011001100110011001100(11) ,最后两位是11 因此进位

X\` - 0.1 表示 

0.00011001100110011001101 

\- 

0.0001100110011000110011001100

= 0.000000000000000000001\[1100\]

其中1100是无限循环 

约等于 2^-22 x 0.1,大约等于2.38x10^-8 

相比0.1 - x , X` - 0.1 所产生的误差, 2.38x10^-8 x 100 x 60 x 60 x 10 ~= 0.086, 相差4倍 速率为300m/s 距离误差为 25.8m

因此进行舍入是有效处理

实际问题解决

浮点 0.1 + 0.2 不等于 0.3

stackoverflow.com/questions/5… stackoverflow.com/questions/3…

解决方式1

允许最大误差 epsilon,适合比较大小使用,这个怎样用?

代码比对,0.1 + 0.2 = 0.3 时候 出现给0.3大的数,这就是浮点精度所产生的误差问题,如,

php -r "var_dump(0.1 + 0.2, (0.1 + 0.2) == 0.3);"

输出 

float(0.30000000000000004) 

bool(false)

这里需要进行两个数比较的时候,需要运行最大误差值,一些语言提供一种 espilion 浮点数可表示的最小值

PHP_FLOAT_EPSILON = 2.220446049250313E-16 

php -r "var_dump((0.1 + 0.2 - 0.3) <= PHP_FLOAT_EPSILON);” 

输出 

bool(false) 

完全转为10进制,忽略浮点,decimal 库,decimal是计算机模拟出来,给原来的浮点运算是慢非常多,一般场景已经满足使用

另外一种方式使用bcmath进行运算

php -r "var_dump(bcadd(0.1,0.2, 17), bcadd(0.1,0.2,17) == 0.3);" 

保留小数点17位的原因是因为 0.1 + 0.2 浮点值为 0.30000000000000004 小数点17位

上面实现,bcadd(0.1,0.2, 17) = 0.30000000000000000 且是字符串

实现了 0.3 比对的时候正确比对

尝试过其他语言如go语言

package main 

import "fmt" 

func main() { 
    f32 := 0.1  + 0.2 
    a := fmt.Sprintf(“%.17f",  f32); 
    fmt.Println(a); 
} 


输出
0.29999999999999999 

如果不指定 小数点多少位,golang 默认帮你提取,如 直接 0.1 + 0.2 = 0.3 是相等的,少了很多问题,一旦指定精度如0.1+0.2 一旦指定保留小数17位就会产生误

猜想golang自己进行后续多个00的时候自动偶数舍入

128位long double, 32位 float 与 double 的存储是怎样处理的##

stackoverflow.com/questions/3…

分开存储,针对这更高精度的存储,需要扩大寄存器的使用存储

为什么 0.1 显示到0.1是 但运算的时候会产生差异 0.100000001490116119384765625 因为输入0.1的时候取整后,运算的时候,是全部位数一起进行运算

银行系统是如何处理浮点的?##

场景: 费用 of --- $1,290

被9人平分 --- $1,290 / 9

每人能获取到: 143.3333333333

如果这个是银行给客户的

银行不会向上取证,如,保留小数点后两位 ,143.33 而不会143.34

如果是客户给银行的 需要给 143.34

计算机存储结构解析

通过 decimal data type 且与舍入位数处理,数据库的decimal 类型

Decimal 结构

mysql 的实现 
http://mysql.taobao.org/monthly/2021/03/02/
struct decimal_t { 
  int intg, frac, len; 
  bool sign; 
  decimal_digit_t *buf; 
}; 


通过 buf 数组进行位数
// decimal(81, 18) 63 个整数数字, 18 个小数数字, 用满整个 buffer// 123456789012345678901234567890123456789012345678901234567890123.012345678901234567 


decimal_t dec_81_digit = { 
  int intg = 63; 
  int frac = 18; 
  int len = 9; 
  bool sign = false; 
  buf = {123456789, 12345678, 901234567, 890123456, 789012345, 678901234, 567890123, 12345678, 901234567} 
}; 

java 的 BigInteger 也是差不多通过 数组,切好小数点的长度放入数组里面 zazalu.space/2019/09/25/…

无符号浮点?

按照IEEE 754的表述,浮点是通过首位0/1表示正负,没有表示为符号型,而且浮点是关心小数点的精度问题 steve.hollasch.net/cgindex/cod…

总结

有限的精度不是主要问题,计算相对误差才是

改善方向,需要提高运算时候的精度,如扩大exp和frac,不断接近真实数

现在知道 为什么 0.1 + 0.2 不等于 0.3

参考资料

浮点二进制科学计算方式表示,描述 E M 的逻辑关系

en.wikipedia.org/wiki/Single… en.wikipedia.org/wiki/Floati…

en.wikipedia.org/wiki/Expone…

medium.com/starbugs/se…

浮点科学计算方式的装换

stackoverflow.com/questions/3…

stackoverflow.com/questions/1…

stackoverflow.com/questions/5…

计算器

www.h-schmidt.net/FloatConver…

www.binaryconvert.com/result_floa…

tool.oschina.net/hexconvert/

银行处理浮点

www.reddit.com/r/webdev/co…

www.zhihu.com/question/22…

en.m.wikipedia.org/wiki/Decima…

bias参考资料

en.wikipedia.org/wiki/Expone…

sites.google.com/site/nutncs…

stackoverflow.com/questions/2….

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1

联系我们