关于php:浮点数精度

PHP - Floating Number Precision

本问题已经有最佳答案,请猛点这里访问。
1
2
3
$a = '35';
$b = '-34.99';
echo ($a + $b);

结果为0.0099999999998

这是怎么回事?我想知道为什么我的程序总是报告奇怪的结果。

为什么php不返回预期的0.01?


因为浮点运算!=实数算术。由于不精确性而产生的差异的一个例子是,对于一些浮动的ab(a+b)-b != a。这适用于使用浮动的任何语言。

因为浮点数是精度有限的二进制数,所以有有限数量的可表示数,这会导致精度问题和这样的意外。这是另一本有趣的书:每一个计算机科学家都应该知道什么是浮点运算。

回到你的问题上,基本上没有办法精确地用二进制表示34.99或0.01(就像十进制一样,1/3=0.3333…),所以使用近似值代替。要解决此问题,您可以:

  • 对结果使用round($result, 2)将其四舍五入到小数点后2位。

  • 使用整数。如果这是货币,比如美元,那么存储35.00美元为3500美元,34.99美元为3499美元,然后将结果除以100。

  • 很遗憾,PHP没有其他语言那样的十进制数据类型。


    浮点数和所有数字一样,必须以0和1的字符串形式存储在内存中。它是计算机的所有位。浮点与整数的区别在于,当我们想查看0和1时,如何解释它们。

    一位是"符号"(0=正,1=负),8位是指数(范围从-128到+127),23位是称为"尾数"(分数)的数字。因此,(s1)(p8)(m23)的二进制表示具有值(-1^s)m*2^p

    "尾数"是一种特殊的形式。在普通的科学记数法中,我们显示"一个人的位置"和分数。例如:

    4.39 x 10^2=439

    在二进制中,"一个人的位置"是一个位。因为我们忽略了科学记数法中所有最左边的0(我们忽略了任何无关紧要的数字),所以第一位肯定是1。

    1.101 x 2^3=1101=13

    因为我们保证第一个位是1,所以在存储数字以节省空间时,我们会删除这个位。所以上面的数字只存储为101(尾数)。假设前导1

    例如,让我们以二进制字符串为例

    1
    00000010010110000000000000000000

    将其分解为组件:

    1
    2
    3
    4
    5
    Sign    Power           Mantissa
     0     00000100   10110000000000000000000
     +        +4             1.1011
     +        +4       1 + .5 + .125 + .0625
     +        +4             1.6875

    应用我们的简单公式:

    1
    2
    3
    4
    (-1^S)M*2^P
    (-1^0)(1.6875)*2^(+4)
    (1)(1.6875)*(16)
    27

    换句话说,00000001011000000000000000000000是27个浮点(根据IEEE-754标准)。

    然而,对于许多数字,没有精确的二进制表示。就像1/3=0.333……永远重复,1/100是0.000001010001111010110000…..重复"1010011111010111000"。然而,32位计算机不能将整个数字存储在浮点中。所以这是最好的猜测。

    1
    2
    3
    4
    5
    6
    7
    0.0000001010001111010111000010100011110101110000

    Sign    Power           Mantissa
     +        -7     1.01000111101011100001010
     0    -00000111   01000111101011100001010
     0     11111001   01000111101011100001010
    01111100101000111101011100001010

    (注意负7是用2的补码产生的)

    应该立即清楚的是,01111001010001111010111000001010与0.01完全不同。

    然而,更重要的是,它包含了重复小数的截断版本。原始的十进制包含一个重复的"10100011111010111000"。我们把它简化为010001111010111000001010。

    通过我们的公式将这个浮点数转换回十进制,我们得到0.0099999979(注意这是针对32位计算机的)。一台64位的计算机会有更高的精度)

    十进制等价物

    如果它有助于更好地理解这个问题,那么让我们看看十进制科学记数法在处理重复的小数时。

    假设我们有10个"盒子"来存储数字。因此,如果我们想存储1/16这样的数字,我们会写:

    1
    2
    3
    +---+---+---+---+---+---+---+---+---+---+
    | + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
    +---+---+---+---+---+---+---+---+---+---+

    这显然只是6.25 e -2,其中e*10^(的缩写。我们为十进制分配了4个框,尽管我们只需要2个(用零填充),并且为符号分配了2个框(一个用于数字的符号,一个用于指数的符号)

    使用这样的10个框,我们可以显示从-9.9999 e -9+9.9999 e +9的数字。

    这对于小数点后4位或更少的数字都可以,但是当我们试图存储一个像2/3这样的数字时会发生什么?

    1
    2
    3
    +---+---+---+---+---+---+---+---+---+---+
    | + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
    +---+---+---+---+---+---+---+---+---+---+

    这个新数字0.66667并不完全等于2/3。实际上,它是由0.000003333...关闭的。如果我们尝试在基3中编写0.66667,我们将得到0.2000000000012...,而不是0.2

    如果我们使用更大的重复小数,比如1/7,这个问题可能会变得更明显。这有6个重复的数字:0.142857142857...

    将其存储到我们的十进制计算机中,我们只能显示其中5个数字:

    1
    2
    3
    +---+---+---+---+---+---+---+---+---+---+
    | + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
    +---+---+---+---+---+---+---+---+---+---+

    这个数字,0.14286是由.000002857...关闭的。

    它是"接近正确的",但它并不完全正确,因此如果我们试图以7为基数写这个数字,我们会得到一个可怕的数字而不是0.1。事实上,把这个插入Wolfram Alpha,我们得到:.10000022320335...

    这些细微的分数差异对您的0.0099999979应该是熟悉的(而不是0.01)。


    这里有很多关于浮点数为什么按它们的方式工作的答案…

    但很少有关于任意精确性的讨论(皮克提到过)。如果您想要(或需要)精确的精度,唯一的方法是使用BC数学扩展(它实际上只是一个bignum,任意精度的实现)。

    要添加两个数字:

    1
    2
    3
    $number = '12345678901234.1234567890';
    $number2 = '1';
    echo bcadd($number, $number2);

    会导致12345678901235.1234567890

    这就是所谓的任意精度数学。基本上,所有的数字都是字符串,对每个操作进行解析,并以数字为基础执行操作(考虑长除法,但由库完成)。这意味着它相当慢(与常规的数学构造相比)。但它非常强大。您可以乘、加、减、除、求模和指数化任何具有精确字符串表示形式的数字。

    所以你不能100%准确地进行1/3,因为它有一个重复的小数(因此是不合理的)。

    但是,如果你想知道1500.0015的平方是什么:

    使用32位浮点(双精度)得出以下估计结果:

    1
    2250004.5000023

    但BCMath给出了以下确切答案:

    1
    2250004.50000225

    这完全取决于你需要的精度。

    另外,这里还有一些需要注意的地方。PHP只能表示32位或64位整数(取决于您的安装)。因此,如果一个整数超过了本机int类型的大小(32位为21亿,有符号int为9.2 x10^18,或92亿),php将把int转换成一个浮点。虽然这并不是一个立即出现的问题(因为所有小于系统浮点精度的整数在定义上都直接表示为浮点数),但如果尝试将两个整数相乘,它将失去显著的精度。

    例如,给定$n = '40000000002'

    作为一个数字,$n将是float(40000000002),这是罚款,因为它完全代表。但如果我们把它平方,我们得到:float(1.60000000016E+21)

    作为一个字符串(使用bc-math),$n正好是'40000000002'。如果我们把它摆正,我们得到:string(22)"1600000000160000000004"

    因此,如果您需要大数字或有理小数点的精度,您可能需要研究BCMath…


    bcadd()在这里可能很有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <?PHP

    $a = '35';
    $b = '-34.99';

    echo $a + $b;
    echo '<br />';
    echo bcadd($a,$b,2);

    ?>

    (为了清晰起见,输出效率低下)

    第一行给出0.0099999999998。第二个给我0.01


    使用php的round()函数:http://php.net/manual/en/function.round.php

    这个答案解决了问题,但不能解释为什么。我认为这是显而易见的[我也在C++编程,所以对我来说是显而易见的],但是如果不是,假设PHP有它自己的计算精度,在那个特定的情况下它返回了关于该计算的最符合的信息。


    [解决]

    enter image description here

    每个数字都将以二进制值(如0、1)保存在计算机中。在单精度数字中占32位。

    浮点数可以表示为:1位表示符号,8位表示指数,23位表示尾数(分数)。

    请看下面的示例:

    0.15625=0.00101=1.01*2^(-3)

    enter image description here

    • 符号:0表示正数,1表示负数,本例为0。

    • 指数:0111100=127-3=124。

      注:偏差=127,所以偏差指数=?3+偏差。在单精度中,偏差为127,因此在本例中,偏差指数为124;

    • 在分数部分,我们有:1.01平均值:0*2^-1+1*2^-2

      数字1(1.01的第一个位置)不需要保存,因为当以这种方式显示浮点数时,第一个数字始终是1。例如,转换:0.11=>1.1*2^(-1),0.01=>1*2^(-2)。

    另一个示例显示始终删除第一个零:0.1将显示1*2^(-1)。所以第一个总是1。当前1*2^(-1)的数字为:

    • 0:正数
    • 127-1=126=0111110
    • 分数:000000000000000000000(23个数字)

    最后:原始二进制文件是:0 0111110亿

    请在此处查看:http://www.binaryconvert.com/result_float.html?小数=048046053

    现在,如果您已经了解如何保存浮点数。如果数字不能以32位(简单的精度)保存,会发生什么情况?

    例如:十进制。1/3=0.33333333333333333333333333,因为它是无限的,我想我们有5位来保存数据。再说一遍,这不是真的。试想一下。因此,保存在计算机中的数据将是:

    1
    0.33333.

    现在,当计算机再次加载数字时,计算:

    1
    0.33333 = 3*10^-1 + 3*10^-2 + 3*10^-3 + 3*10^-4 +  3*10^-5.

    关于这一点:

    1
    2
    3
    $a = '35';
    $b = '-34.99';
    echo ($a + $b);

    结果是0.01(十进制)。现在让我们用二进制显示这个数字。

    1
    0.01 (decimal) = 0 10001111 01011100001010001111 (01011100001010001111)*(binary)

    请访问:http://www.binaryconvert.com/result_double.html?小数=048046048049

    因为(01011100000100001111)的重复次数和1/3一样。所以计算机无法将这个号码保存在内存中。它必须牺牲。这导致了计算机的不精确。

    先进的(你必须有数学知识)所以为什么我们可以很容易地用十进制而不是二进制显示0.01。

    假设二进制0.01(十进制)的分数是有限的。

    1
    2
    3
    4
    So 0.01 = 2^x + 2^y... 2^-z
    0.01 * (2^(x+y+...z)) =  (2^x + 2^y... 2^z)*(2^(x+y+...z)). This expression is true when (2^(x+y+...z)) = 100*x1. There are not integer n = x+y+...+z exists.

    => So 0.01 (decimal) must be infine in binary.


    我的php返回0.01…alt text

    也许它与PHP版本有关(我使用5.2)


    因为0.01不能精确地表示为二元分数系列的和。浮点数就是这样存储在内存中的。

    我想这不是你想听到的,而是问题的答案。有关如何修复的信息,请参阅其他答案。


    使用number_format(0.009999999999998, 2)$res = $a+$b; -> number_format($res, 2);不是更容易吗?