一文了解计算机浮点数精度问题


写PHP遇到问题

开始在计算金额的时候,我一般都使用分来计算,数据库保存也已分为单位(微信支付、支付宝支付 业务只精确到分)

通常我的做法是 乘100 然后保存,原本以为这样写是没有问题的,结果却是写了一个bug,我把浮点型转换为整形的时候

<?php
echo 16.33 * 100; // 1633 float
echo intval(16.33 * 100); // 1632 int
echo intval(strval(16.33 * 100)); // 1633
echo (int)(16.33 * 100); // 1632

然后去复习了一遍计算机中的浮点数(该打)

浮点数在计算机如何保存

之所以产生这样的问题,其根本原因是:计算机所使用二进制01代码无法准确表示某些带小数位的十进制数据。

计算机如何保存浮点数?

  1. 整数部分:连续用该整数除以2,取余数,然后商再除以2,直到商等于0为止。然后把得到的各个余数按相反的顺序排列。简称除2取余法

  2. 小数部分:十进制小数转换为二进制小数,采用乘2取整,顺序排列法。用2乘以十进制小数,将得到的整数部分取出,再用2乘余下的小数部分,
    然后再将积的整数部分取出,如此进行,直到积中的小数部分为0或者达到所要求的精度为止。
    然后把取出的整数部分按顺序排列起来,即先取出的整数部分作为二进制小数的高位,后取出的整数部分作为低位有效位。简称乘2取整法

  3. 含有小数的十进制数转换成二进制,整数、小数部分分别进行转换,然后相加

例如将十进制 29.75 转换为二进制,步骤如下

整数部分

29/2 = 14...1
14/2 = 7...0
7/2 = 3...1
3/2 = 1...1
1/2 = 0...1

29(10) = 11101(2)

小数部分

0.75*2 = 1.5...1
0.5*2 = 1...1

0.75(10) = 0.11(2)

29.75(10) = 11101.11(2)

按照上面的算法,我们来计算 16.33,我们只需要验证小数部分就行

0.33*2 = 0.66...0
0.66*2 = 1.32...1
0.32*2 = 0.64...0
0.64*2 = 1.28...1
0.28*2 = 0.56...0
0.56*2 = 1.12...1
0.12*2 = 0.24...0
0.24*2 = 0.48...0
0.48*2 = 0.96...0
0.96*2 = 1.92...1
0.92*2 = 1.84...1
.... 

发现是算不完的

目前计算机上存储浮点数值是按照IEEE(电气和电子工程师协会)754浮点存储格式标准来存储的

IEEE单精度浮点格式共32位,包含三个构成字段:
23位小数f,8位偏置指数e,1位符号s。将这些字段连续存放在一个32位字里,并对其进行编码。其中0:22位包含23位的小数f;23:30位包含8位指数e;第31位包含符号s

也就是说上面将0.33转换出的二进制代码,我们只能存储23位,即使数据类型为double,也只能存储52位,这样大家便能看出问题出现的原因了。

截取的二进制代码已无法正确表示0.33,根据这个二进制代码肯定无法正确得到结果

如何解决

  1. 因为二进制数值可以准确表示整数(可以使用整数转换为二进制方法验证下),所以可以将小数乘以10或100等变成整数,然后做运算,最后再通过除以10或100等获得结果。

  2. 通过截取结果的有效小数位数等,来取得最好的近似结果,然后在做处理。

  3. 对于可以用有限长度的二进制数值表示的十进制数值,可以使用存储位数大于其长度的数据类型。


文章作者: 江湖义气
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 江湖义气 !
  目录