请稍侯

浮点数剖析 (PHP)

18 August 2017

PHP面试中, 经常会被问到的一个问题

<?php
    $f = 0.58;
    var_dump(intval($f * 100)); 
?>

上面输出的结果是57, 而不是58, 为什么呢, 因为 你看似有穷的小数, 在计算机的二进制表示里却是无穷的(鸟哥的原话),0.58用二进制后, 重新计算出来的值是:0.57999999999999996, 所以乘以100之后,去整数部分,就是57了。

代码中的intval改为floor后,输出的结果也是57

名字解释

  • BC是Binary Calculator的缩写, 二进制计算器
  • Sign 符号
  • Exponent 指数
  • Fraction 小数
  • IEEE 英语:Institute of Electrical and Electronics Engineers 电子技术与电子工程师协会,简称为IEEE。 IEEE is the world’s largest technical professional organization dedicated to advancing technology for the benefit of humanity. Below, you can find IEEE’s mission and vision statements.

参考文章,鸟哥的两篇文章外加IEEE 754

IEEE 754 全称为,IEEE二进制浮点数算术标准, 此标准中,规定了浮点数二进制表示的规范:

浮点数二进制表示包括三部分,

  1. 符号位, 用1个字节来表示
  2. 指数位,
  3. 有效数字

如:

  • 单精度浮点数共32位(bit),1bit的符号位,8bit指数位,23bit有效数字
  • 双精度浮点数共64位(bit),1bit的符号位,11bit指数位,52bit有效数字

浮点数表示为二进制的计算方式是: 浮点数二进制表示学习笔记

整数部分除以2取余,然后再用所得的商除以2取余,一直到商为0,并且逆序排列所得的余数; 小数部分乘以2取整数部分,然后再用新的小数部分乘以二,取整数,一直到新的小数部分为0, 或者达到了要求的精度为止, 并且顺序排列所得的整数部分。

浮点数转化为二进制的例子

  1. 10.625转化为二进制

整数部分10, 对2求余, 商继续对2求余,直到商为0, 再逆序排列每一步得到的余数

计算 余数
10/2 0 5
5/2 1 2
2/2 0 1
1/2 1 0

10的二进制表示为1010

小数部分0.625, 乘以2, 取整数部分,新的小数部分继续乘以2, 直到新的小数部分为0或者达到一定精度,再顺序排列每一步得到整数部分。

计算 整数部分 小数部分
0.625 * 2 = 1.25 1 0.25
0.25 * 2 = 0.5 0 0.5
0.5 * 2 = 1 1 0

0.625的二进制表示为101

  1. 0.58的二进制表示 比如要求的精度是用53位来表示这个小数,可以得到如下表格:
计算 整数部分 小数部分
0.58 * 2 = 1.16 1 0.16
0.16 * 2 = 0.32 0 0.32
0.32 * 2 = 0.64 0 0.64
0.64 * 2 = 1.28 1 0.28
0.28 * 2 = 0.56 0 0.56
0.56 * 2 = 1.12 1 0.12
0.12 * 2 = 0.24 0 0.24
0.24 * 2 = 0.48 0 0.48
0.48 * 2 = 0.96 0 0.96
0.96 * 2 = 1.92 1 0.92
0.92 * 2 = 1.84 1 0.84
0.84 * 2 = 1.68 1 0.68
0.68 * 2 = 1.36 1 0.36
0.36 * 2 = 0.72 0 0.72
0.72 * 2 = 1.44 1 0.44
0.44 * 2 = 0.88 0 0.88
0.88 * 2 = 1.76 1 0.76
0.76 * 2 = 1.52 1 0.52
0.52 * 2 = 1.04 1 0.04
0.04 * 2 = 0.08 0 0.08
0.08 * 2 = 0.16 0 0.16
0.16 * 2 = 0.32 0 0.32
0.32 * 2 = 0.64 0 0.64
0.64 * 2 = 1.28 1 0.28
0.28 * 2 = 0.56 0 0.56
0.56 * 2 = 1.12 1 0.12
0.12 * 2 = 0.24 0 0.24
0.24 * 2 = 0.48 0 0.48
0.48 * 2 = 0.96 0 0.96
0.96 * 2 = 1.92 1 0.92
0.92 * 2 = 1.84 1 0.84
0.84 * 2 = 1.68 1 0.68
0.68 * 2 = 1.36 1 0.36
0.36 * 2 = 0.72 0 0.72
0.72 * 2 = 1.44 1 0.44
0.44 * 2 = 0.88 0 0.88
0.88 * 2 = 1.76 1 0.76
0.76 * 2 = 1.52 1 0.52
0.52 * 2 = 1.04 1 0.04
0.04 * 2 = 0.08 0 0.08
0.08 * 2 = 0.16 0 0.16
0.16 * 2 = 0.32 0 0.32
0.32 * 2 = 0.64 0 0.64
0.64 * 2 = 1.28 1 0.28
0.28 * 2 = 0.56 0 0.56
0.56 * 2 = 1.12 1 0.12
0.12 * 2 = 0.24 0 0.24
0.24 * 2 = 0.48 0 0.48
0.48 * 2 = 0.96 0 0.96
0.96 * 2 = 1.92 1 0.92
0.92 * 2 = 1.84 1 0.84
0.84 * 2 = 1.68 1 0.68
0.68 * 2 = 1.36 1 0.36

0.58 的二进制为: 10010100011110101110000101000111101011100001010001111

如上表格是通过如下 程序简单生成的:

$f = 0.58;
$b = '';
$i = 0;
while(true) {
    if ($i > 52) {
        break;
    }
    $i++;
    $str = "$f * 2 ";
    $tmp = $f * 2;
    $int = intval($tmp);
    $b .= $int;
    $f = round($tmp - $int, 3);
    $str .= "= $tmp | $int | $f \n";
    echo $str;
    if ($f == 0) {
        break;
    }
}

浮点数比较

看似两个相等的浮点数,其实进行比较时, 可能不想等了。

$f = 0.58;
$f2 = 1 - 0.42;
var_dump($f == $f2);
printf("%.21f \n", $f);
printf("%.21f \n", $f2);

如上代码大家觉着会输出什么呢? 其实输出的结果是:

bool(false)
0.579999999999999960032
0.580000000000000071054

所以在做浮点数比较的时候,要谨慎处理, 或者round四舍五入之后再比较。

精度输出

for($i=1;$i<=55; $i++) {
    printf("%d %.{$i}f\n", $i, 0.58);
}

如上代码,输出结果为:

1 0.6
2 0.58
3 0.580
4 0.5800
5 0.58000
6 0.580000
7 0.5800000
8 0.58000000
9 0.580000000
10 0.5800000000
11 0.58000000000
12 0.580000000000
13 0.5800000000000
14 0.58000000000000
15 0.580000000000000
16 0.5800000000000000
17 0.57999999999999996
18 0.579999999999999960
19 0.5799999999999999600
20 0.57999999999999996003
21 0.579999999999999960032
22 0.5799999999999999600320
23 0.57999999999999996003197
24 0.579999999999999960031971
25 0.5799999999999999600319711
26 0.57999999999999996003197111
27 0.579999999999999960031971113
28 0.5799999999999999600319711135
29 0.57999999999999996003197111349
30 0.579999999999999960031971113494
31 0.5799999999999999600319711134944
32 0.57999999999999996003197111349436
33 0.579999999999999960031971113494365
34 0.5799999999999999600319711134943645
35 0.57999999999999996003197111349436454
36 0.579999999999999960031971113494364545
37 0.5799999999999999600319711134943645447
38 0.57999999999999996003197111349436454475
39 0.579999999999999960031971113494364544749
40 0.5799999999999999600319711134943645447493
41 0.57999999999999996003197111349436454474926
42 0.579999999999999960031971113494364544749260
43 0.5799999999999999600319711134943645447492599
44 0.57999999999999996003197111349436454474925995
45 0.579999999999999960031971113494364544749259949
46 0.5799999999999999600319711134943645447492599487
47 0.57999999999999996003197111349436454474925994873
48 0.579999999999999960031971113494364544749259948730
49 0.5799999999999999600319711134943645447492599487305
50 0.57999999999999996003197111349436454474925994873047
51 0.579999999999999960031971113494364544749259948730469
52 0.5799999999999999600319711134943645447492599487304688
53 0.57999999999999996003197111349436454474925994873046875
PHP Notice:  printf(): Requested precision of 54 digits was truncated to PHP maximum of 53 digits in /data/cweb/2870000/campus_debug/website/v1/campus.imqq.cn/yongdehu_dev_api/protected/test.php on line3
54 0.57999999999999996003197111349436454474925994873046875
PHP Notice:  printf(): Requested precision of 55 digits was truncated to PHP maximum of 53 digits in /data/cweb/2870000/campus_debug/website/v1/campus.imqq.cn/yongdehu_dev_api/protected/test.php on line3
55 0.57999999999999996003197111349436454474925994873046875

其中

  1. i为54 和 55 时, PHP给了提示,并且输出结果和i=53时是一样的,最大支持小数点后53个小数。
  2. i 为1时,输出的是0.58四舍五入为只有一位小数的值,
  3. i 为17时才出现了0.57, 说明从16位之前2位之后的所有位四舍五入之后都是0.58

printf在输出浮点数时,会根据设定的位数来做四舍五入。

代码仅仅演示使用,文章内容不保证没有问题, 仅供参考。 欢迎交流指正。