您的位置 首页 php

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

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

所以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

关于阶码 E 的解析

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

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.38×10^-8

相比0.1 – x , X` – 0.1 所产生的误差, 2.38×10^-8 x 100 x 60 x 60 x 10 ~= 0.086, 相差4倍

速率为300m/s 距离误差为 25.8m

因此进行舍入是有效处理

实际问题解决

浮点 0.1 + 0.2 不等于 0.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 的存储是怎样处理的

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

为什么 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 的实现

 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 也是差不多通过 数组,切好小数点的长度放入数组里面

无符号浮点?

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

总结

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

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

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

参考资料

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

#Floating-point_arithmetic_operations

浮点科学计算方式的转换

计算器

银行处理浮点

bias参考资料

#:~:text=By%20using%20a%20bias%20representation,looks%20like%20a%20smaller%20integer.&text=As%20you%20can%20see%2C%20even,out%20in%20the%20correct%20order.

文章来源:智云一二三科技

文章标题:深入理解计算机系统 – 浮点 – 0.1 + 0.2 不等于 0.3?

文章地址:https://www.zhihuclub.com/150025.shtml

关于作者: 智云科技

热门文章

网站地图