V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xiaoyanbot
V2EX  ›  PHP

不使用 bcmath 系列函数,关于 PHP 中 float 乘法的精度问题 的再困惑

  •  
  •   xiaoyanbot · 2017-10-05 22:35:41 +08:00 · 3153 次点击
    这是一个创建于 2366 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在不使用 bcmath 系列函数下,为何精准二进制打印出来是失真的,但最后计算结果却是对的, 为何?

    (使用的 PHP 版本 5.4、5.6 和 7.1 得到的结果一致 )

    以下运算,模拟 $a 为商品价格(带两位小数), $b 为数量(整数)。 困惑的是, 是否这一类的电商运算都采用 bcmath 系列函数来做? MySQL 后端存储为 decimal 类型,这个没疑惑。

    先看看结果为:

    代码为:

    <?php
    
    $a = 1.23;
    $b = 100000;
    
    printf("%0.30f", $a);
    echo '<br />';
    printf("%0.30f", $b);
    
    echo "<hr>";
    echo '$a 的数据类型:  ';
    var_dump($a);echo "<hr>";
    echo '$b 的数据类型:  ';
    var_dump($b);echo "<hr>";
    
    $total = $a * $b;
    
    echo '计算结果:  ';
    var_dump($total);echo "<hr>";
    echo '高精度计算结果:  ';
    printf("%0.30f", $total);
    
    第 1 条附言  ·  2017-10-06 10:52:12 +08:00
    两个相关联的问题:

    只需要保证小数点后两位计算正确, 用 float 有问题吗?
    https://www.v2ex.com/t/373784

    储存价格的字段,单位用元还是分好
    https://www.v2ex.com/t/199222
    12 条回复    2017-10-08 17:38:33 +08:00
    xiaoyanbot
        1
    xiaoyanbot  
    OP
       2017-10-05 22:38:33 +08:00
    难道 * 这个运算符, 自动作了四舍五入的处理了?
    xiaoyanbot
        2
    xiaoyanbot  
    OP
       2017-10-05 22:40:50 +08:00
    又进一步测试,发现当 $b 为 10 的时候, 最终结果是失真的

    结果为:
    ~~~
    1.229999999999999982236431605997
    10.000000000000000000000000000000
    $a 的数据类型:float(1.23)
    $b 的数据类型:int(10)
    计算结果:float(12.3)
    高精度计算结果:12.300000000000000710542735760100
    ~~~

    当$b 为 100 的时候,最终结果不失真

    结果为:
    ~~~
    1.229999999999999982236431605997
    100.000000000000000000000000000000
    $a 的数据类型:float(1.23)
    $b 的数据类型:int(100)
    计算结果:float(123)
    高精度计算结果:123.000000000000000000000000000000
    ~~~
    WuwuGin
        3
    WuwuGin  
       2017-10-06 00:40:25 +08:00
    PHP 遵循 IEEE 754 双精度:
    浮点数, 以 64 位的双精度, 采用 1 位符号位(E), 11 指数位(Q), 52 位尾数(M)表示(一共 64 位).
    符号位:最高位表示数据的正负,0 表示正数,1 表示负数。
    指数位:表示数据以 2 为底的幂,指数采用偏移码表示
    尾数:表示数据小数点后的有效数字.
    再来看看小数用二进制怎么表示:
    乘 2 取整,顺序排列,即将小数部分乘以 2,然后取整数部分,剩下的小数部分继续乘以 2,然后取整数部分,剩下的小数部分又乘以 2,一直取到小数部分,但是像 0.57 这样的小数像这样一直乘下去,小数部分不可能为 0.有效位的小数用二进制表示却是无穷的。
    0.57 的二进制表示基本上(52 位)是: 0010001111010111000010100011110101110000101000111101
    如果只有 52 位的话,0.57 =》 0.56999999999999995
    不难看出上面意外的结果了吧。
    ref:http://www.jb51.net/article/65984.htm
    Sikoay
        4
    Sikoay  
       2017-10-06 00:44:51 +08:00 via Android
    难道不是都采用整数来进行运算和存储吗,float 永远无法准确地计算一个数(忘记是不是这样说的了,effective Java 中有写过
    Sikoay
        5
    Sikoay  
       2017-10-06 00:49:35 +08:00 via Android   ❤️ 1
    我们是将金额乘以 100 (精确到小数点两位后),然后运算和存储,显示的时候再将计算之后的值除以 100
    eoo
        6
    eoo  
       2017-10-06 09:40:27 +08:00 via Android
    楼上+1 腾讯也是这么做滴
    xiaoyanbot
        7
    xiaoyanbot  
    OP
       2017-10-06 10:36:45 +08:00
    @eoo 能确认: 腾讯内部都是这么做的吗?

    有没有人做过使用 bcmath 系列函数,和 采用 int 乘再除 这两种方式的压测效率之类的?
    xiaoyanbot
        8
    xiaoyanbot  
    OP
       2017-10-06 10:37:06 +08:00
    阿里的 Java 手册,关于这一部分的描述
    ~~~
    6. [强制] 小数类型为 decimal,禁止使用 float 和 double。
    说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不
    正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
    ~~~
    eoo
        9
    eoo  
       2017-10-06 11:06:23 +08:00
    @xiaoyanbot 其他我不知道 我只知道 我抓 QQ 支付记录包的时候 返回的都是 支付金额 x 100 你想一下 QQ 支付系统都这样做了 你觉得其他的会有什么不一样?
    Sikoay
        10
    Sikoay  
       2017-10-06 12:37:19 +08:00 via Android
    @eoo 写过微信支付的都知道这一点吧
    blackshadow
        11
    blackshadow  
       2017-10-06 16:39:26 +08:00 via Android
    楼上说的,支付用金额 x100。是因为人民币最小单位是分,这样计算的金额,利息等比较准确。几乎所有的支付系统都是按分存取的吧。在我碰到的 PHP 支付系统里确实有使用了 bcmath 系统函数。至少我做过的是这样。
    xiaoyanbot
        12
    xiaoyanbot  
    OP
       2017-10-08 17:38:33 +08:00
    @blackshadow

    请问有测试过使用 bcmath 函数 和 使用 *100, 再除以 100 的运算效率对比吗?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   976 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 21:55 · PVG 05:55 · LAX 14:55 · JFK 17:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.