PHP是一种弱类型语言, 这样的特性, 必然要求有无缝透明的隐式类型转换, PHP内部使用zval来保存任意类型的数值, zval的结构如下(5.2为例):

  1. struct _zval_struct {
  2. /* Variable information */
  3. zvalue_value value; /* value */
  4. zend_uint refcount;
  5. zend_uchar type; /* active type */
  6. zend_uchar is_ref;
  7. };

上面的结构中, 实际保存数值本身的是zvalue_value联合体:

  1. typedef union _zvalue_value {
  2. long lval; /* long value */
  3. double dval; /* double value */
  4. struct {
  5. char *val;
  6. int len;
  7. } str;
  8. HashTable *ht; /* hash table value */
  9. zend_object_value obj;
  10. } zvalue_value;

今天的话题, 我们只关注其中的俩个成员, lval和dval, 我们要意识到, long lval是随着编译器, OS的字长不同而不定长的, 它有可能是32bits或者64bits, 而double dval(双精度)由IEEE 754规定, 是定长的, 一定是64bits.

请记住这一点, 造就了PHP的一些代码的”非平台无关性”. 我们接下来的讨论, 除了特别指明, 都是假设long为64bits

IEEE 754的浮点计数法, 我这里就不引用了, 大家有兴趣的可以自己查看, 关键的一点是, double的尾数采用52位bit来保存, 算上隐藏的1位有效位, 一共是53bits.

在这里, 引出一个很有意思的问题, 我们用c代码举例(假设long为64bits):

  1. long a = x;
  2. assert(a == (long)(double)a);

请问, a的取值在什么范围内的时候, 上面的代码可以断言成功?(留在文章最后解答)

现在我们回归正题, PHP在执行一个脚本之前, 首先需要读入脚本, 分析脚本, 这个过程中也包含着, 对脚本中的字面量进行zval化, 比如对于如下脚本:

  1. <?php
  2. $a = 9223372036854775807; //64位有符号数最大值
  3. $b = 9223372036854775808; //最大值+1
  4. var_dump($a);
  5. var_dump($b);

输出:

  1. int(9223372036854775807)
  2. float(9.22337203685E+18)

也就说, PHP在词法分析阶段, 对于一个字面量的数值, 会去判断, 是否超出了当前系统的long的表值范围, 如果不是, 则用lval来保存, zval为IS_LONG, 否则就用dval表示, zval IS_FLOAT.

凡是大于最大的整数值的数值, 我们都要小心, 因为它可能会有精度损失:

  1. <?php
  2. $a = 9223372036854775807;
  3. $b = 9223372036854775808;
  4. var_dump($a === ($b - 1));

输出是false.

现在接上开头的讨论, 之前说过, PHP的整数, 可能是32位, 也可能是64位, 那么就决定了, 一些在64位上可以运行正常的代码, 可能会因为隐形的类型转换, 发生精度丢失, 从而造成代码不能正常的运行在32位系统上.

所以, 我们一定要警惕这个临界值, 好在PHP中已经定义了这个临界值:

  1. <?php
  2. echo PHP_INT_MAX;
  3. ?>

当然, 为了保险起见, 我们应该使用字符串来保存大整数, 并且采用比如bcmath这样的数学函数库来进行计算.

另外, 还有一个关键的配置, 会让我们产生迷惑, 这个配置就是php.precision, 这配置决定了PHP再输出一个float值的时候, 输出多少有效位.

最后, 我们再来回头看上面提出的问题, 也就是一个long的整数, 最大的值是多少, 才能保证转到float以后再转回long不会发生精度丢失?

比如, 对于整数, 我们知道它的二进制表示是, 101, 现在, 让我们右移俩位, 变成1.01, 舍去高位的隐含有效位1, 我们得到在double中存储5的二进制数值为:

  1. 0/*符号位*/ 10000000001/*指数位*/ 0100000000000000000000000000000000000000000000000000

5的二进制表示, 丝毫未损的保存在了尾数部分, 这个情况下, 从double转会回long, 不会发生精度丢失.

我们知道double用52位表示尾数, 算上隐含的首位1, 一共是53位精度.. 那么也就可以得出, 如果一个long的整数, 值小于:

  1. 2^53 - 1 == 9007199254740991; //牢记, 我们现在假设是64bits的long

那么, 这个整数, 在发生long->double->long的数值转换时, 不会发生精度丢失.

  1. <?php
  2. $f = 0.58;
  3. var_dump(intval($f * 100)); //为啥输出57
  4. ?>

为啥输出是57啊? PHP的bug么?

我相信有很多的同学有过这样的疑问, 因为光问我类似问题的人就很多, 更不用说bugs.php.net上经常有人问…

要搞明白这个原因, 首先我们要知道浮点数的表示(IEEE 754):

浮点数, 以64位的长度(双精度)为例, 会采用1位符号位(E), 11指数位(Q), 52位尾数(M)表示(一共64位).

符号位:最高位表示数据的正负,0表示正数,1表示负数。

指数位:表示数据以2为底的幂,指数采用偏移码表示

尾数:表示数据小数点后的有效数字.

这里的关键点就在于, 小数在二进制的表示, 关于小数如何用二进制表示, 大家可以百度一下, 我这里就不再赘述, 我们关键的要了解, 0.58 对于二进制表示来说, 是无限长的值(下面的数字省掉了隐含的1)..

  1. 0.58的二进制表示基本上(52位)是: 0010100011110101110000101000111101011100001010001111
  2. 0.57的二进制表示基本上(52位)是: 0010001111010111000010100011110101110000101000111101

而两者的二进制, 如果只是通过这52位计算的话,分别是:

  1. 0.58 -> 0.57999999999999996
  2. 0.57 -> 0.56999999999999995

至于0.58 * 100的具体浮点数乘法, 我们不考虑那么细, 有兴趣的可以看(Floating point), 我们就模糊的以心算来看… 0.58 * 100 = 57.999999999

那你intval一下, 自然就是57了….

可见, 这个问题的关键点就是: “你看似有穷的小数, 在计算机的二进制表示里却是无穷的”

so, 不要再以为这是PHP的bug了, 这就是这样的…..

关于PHP浮点数之 intval((0.1+0.7)*10) 为什么是7的更多相关文章

  1. 设置浮点数的显示精度&precision(0)

    /*    设置浮点数的显示精度    cout.precision(int)可以设置浮点数的显示精度(不包括小数点)        注: 1.如果设置的精度大于浮点数的位数,如果浮点数能根据IEEE ...

  2. 震惊!计算机连0.3+0.6都算不对?浅谈IEEE754浮点数算数标准

    >>> 0.3+0.6 0.8999999999999999 >>> 1-0.9 0.09999999999999998 >>> 0.1+0.1+ ...

  3. 如何避开JavaScript浮点数计算精度问题(如0.1+0.2!==0.3)

    不知道大家在使用JS的过程中有没有发现某些浮点数运算的时候,得到的结果存在精度问题:比如0.1 + 0.2 = 0.30000000000000004以及7 * 0.8 = 5.60000000000 ...

  4. iOS - 浮点数去掉小数点之后的0,以0.5小数递增函数

    NSMutableArray *arrNumber = [NSMutableArray array]; float a = -0.5; ) { a+=0.5; NSString *str = [sel ...

  5. 【js奇妙说】如何跟非计算机从业者解释,为什么浮点数计算0.1+0.2不等于0.3?

    壹 ❀ 引 0.1+0.2不等于0.3,即便你不知道原理,但也应该听闻过这个问题,包括博主本人也曾在面试中被问到过此问题.很遗憾,当时只知道一句精度丢失,但是什么原因造成的精度丢失却不太清楚.而我在查 ...

  6. 《征服 C 指针》摘录1:什么是空指针?区分 NULL、0 和 '\0'

    一.什么是空指针? 空指针 是一个特殊的指针值. 空指针 是指可以确保没有向任何一个对象的指针.通常使用宏定义 NULL 来表示空指针常量值. 空指针 确保它和任何非空指针进行比较都不会相等,因此经常 ...

  7. JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后

    Brief 一天有个朋友问我“JS中计算0.7 * 180怎么会等于125.99999999998,坑也太多了吧!”那时我猜测是二进制表示数值时发生round-off error所导致,但并不清楚具体 ...

  8. 如何解决JavaScript中0.1+0.2不等于0.3

    console.log(0.1+0.2===0.3)// true or false?? 在正常的数学逻辑思维中,0.1+0.2=0.3这个逻辑是正确的,但是在JavaScript中0.1+0.2!= ...

  9. console.log(0.2+0.4===0.6)// true or false??

    在正常的数学逻辑思维中,0.2+0.4===0.6这个逻辑是正确的,但是在JavaScript中0.2+0.4!==0.6这是为什么呢?这个问题也会偶尔被用来当做面试题来考查面试者对 JavaScri ...

随机推荐

  1. Elasticsearch之curl删除索引库

    关于curl创建索引库的介绍,请移步 Elasticsearch之curl创建索引库 [hadoop@djt002 elasticsearch-2.4.3]$ curl -XPUT 'http://1 ...

  2. SQlserver 当输入参数为可选条件

    以前很懒,都是用拼接字符串的方式,加上if 语句,根据输入参数是否为空来判断是否需要在where 后加上对应字段的条件限制 但是拼接字符串很烦,又总是被转义符搞得很烦  '''' 所以想了其他办法 分 ...

  3. C# Socket通讯 本机多网卡,指定网卡通讯

    IPAddress ip = IPAddress.Parse("192.168.0.188"); IPAddress IPLocal = IPAddress.Parse(" ...

  4. Deutsch lernen (11)

    1. anwesend a. 出席的,在场的 ~ abwesend Es waren gegen 50 Leute anwesend. 2. gespannt a. (心情)急切的,急于想知道的:紧张 ...

  5. Deutsch lernen (05)

    1. die Wahrheit, -en 真理:  - 真言,实情 Wir sollen die Wahrheit festhalten. 坚持:紧握 Im Wein liegt Wahrheit. ...

  6. SLAM:飞行机器人的参数解析-分类

    在水电站存在的山区,公路运输效率极低,盘山公路绕行消耗大量时间,使用飞行机器人进行运输是合适的选择. 实现一位长辈在山区飞行的愿望,任重而道远 常见飞行机器人的参数解析:解读飞行机器人的基本类型及技术 ...

  7. AI:忧郁的机器人

    1.塔奇克马 塔奇克马研究起来哲学,被缴械....... 2.机器人瓦力 孤独等待EVA的瓦力 3.马文 http://www.guokr.com/post/683881/

  8. Java中Json的用法

    1. JSONObject json = new JSONObject(); json.put("list", "test"); msg=json.toStri ...

  9. golang入门-defer

    package main import "fmt" func main() { i := 5 tmap := make(map[string]int, 5) tmap[" ...

  10. vue 中引入Jquery插件

    import $ from '../../static/js/jquery.min.js' window.jQuery = $; require('../../static/js/jquery.zoo ...