前言:
如今小伙伴们对“php两时间相减”大致比较着重,你们都需要知道一些“php两时间相减”的相关知识。那么小编也在网上搜集了一些有关“php两时间相减””的相关知识,希望我们能喜欢,咱们一起来了解一下吧!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.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
解决方式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 的逻辑关系
浮点科学计算方式的转换
计算器
银行处理浮点
bias参考资料
.