9 题: PHP - 浮点数精度[重复]

在...创建的问题 Sat, Jul 30, 2016 12:00 AM
    

这个问题已经有了答案:

    
  •             浮动点数学是否已损坏?                                      30个答案                 跨度>         
  •     
 
$a = '35';
$b = '-34.99';
echo ($a + $b);

结果为0.009999999999998

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

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

    
75
  1. 我建议阅读浮点数。具体来说,“可表示的数字,转换和舍入”“准确性问题”。如果你想了解它们是如何工作的,那么本文的其余部分是好的,但这两部分专门适用于你的问题...
    2010-09-16 12:44:52Z
  2. 值得注意的是,您使用的是字符串而不是数字(它们会被隐式转换,但仍然存在)。改为$a = 35; $b = -34.99
    2010-09-16 13:41:56Z
  3. 2018-09-04 02:13:17Z
  4. 醇>
    9答案                              9 跨度>                         

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

    由于浮点数是具有有限精度的二进制数,因此可表示的数字,其中准确性问题和这样的惊喜。这是另一个有趣的读物:每个计算机科学家应该知道的关于浮点运算的内容


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

    1. 对结果使用 round($result, 2) 将其四舍五入为2位小数地方。

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

    3. 醇>

      遗憾的是PHP没有像其他的小数据类型语言

          
    117
    2017-12-11 23:39:47Z
    1. 我想补充说0.01也不能按原样表示。这应该标记为正确,因为它给出了解释和如何解决。但为了增加它的实用性,请解释为什么fp!= real,所有二进制内容和精度丢失
      2010-09-16 12:52:06Z
    2. @ irc谢谢。我将你的评论中的一些内容纳入了答案
      2010-09-16 13:11:11Z
    3. 一个迂腐的说明:有一组有限的浮点数ab,其中(a+b)-b == a。它们只需要具有2的素因子,并且可以用适当的位数表示(单精度约为7位小数,双精度为16)。因此a = 0.5b = 0.25可以工作(并且将始终适用于具有32位单精度浮点数的系统)。对于那些不符合这两个前提条件的浮标,则为(a+b)-b != a。但如果ab都符合这些先决条件,那么(a+b)-b == a应该是真的(但它是一个有限集)...
      2010-09-16 13:17:14Z
    4. @ irc True;我在那里用错了字。
      2010-09-16 13:19:24Z
    5. 我给+1,但是有更多的链接和解释比我想要的少。或许提到二进制的十进制值0.01具有重复的“10100011110101110000”(该数字看起来像0.00000010100011110101110000 .....)。然后进一步解释一个32位计算机仅限于表示23位有效数字(指数为8,符号为1 = 32位),这意味着它变为0.00000010100011110101110000101 = d0.0099999979
      2010-09-29 02:32:34Z
    6. 醇>

    浮点数与所有数字一样,必须以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

    举个例子,我们来看二进制字符串

     
    00000010010110000000000000000000
    

    将其分解为组件:

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

    应用我们的简单公式:

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

    换句话说,00000010010110000000000000000000浮点数为27(根据IEEE-754标准)。

    然而,对于许多数字,没有确切的二进制表示。很像1/3 = 0.333 ....永远重复,1/100是0.00000010100011110101110000 .....重复“10100011110101110000”。但是,32位计算机无法以浮点存储整个数字。所以它是最好的猜测。

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

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

    应该立即明白,01111100101000111101011100001010看起来不像0.01

    然而,更重要的是,它包含重复小数的截断版本。原始十进制包含重复的“10100011110101110000”。我们已将其简化为01000111101011100001010

    通过我们的公式将此浮点数转换回十进制,我们得到0.0099999979(请注意,这是针对32位计算机.64位计算机将具有更高的准确性)

    十进制等效

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

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

     
    +---+---+---+---+---+---+---+---+---+---+
    | + | 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这样的数字时会发生什么?

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

    这个新号码0.66667并不完全等于2/3。事实上,这是0.000003333...。如果我们尝试在base 3中编写0.66667,我们将获得 0.2000000000012... 而不是0.2

    如果我们采用具有较大重复小数的东西,例如1/7,这个问题可能会变得更加明显。这有6个重复的数字:0.142857142857...

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

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

    这个数字0.14286.000002857...关闭

    它“接近正确”,但它不是 完全正确的 ,所以如果我们试图在基数7中写这个数字,我们会得到一些可怕的数字,而不是0.1事实上,将其插入Wolfram Alpha我们得到: .10000022320335...

    这些小的分数差异看起来应该与您的0.0099999979相比(而不是0.01

        
    47
    2018-11-29 20:02:31Z
    1. + 1谢谢,现在我知道如何存储浮点数。 PS:不,Windows没有。至少在PHP5.3.1 /Win7中我做了有浮点问题;)
      2010-09-29 16:56:05Z
    2. 应删除最后一段(声称操作系统决定是否舍入浮点值)。浮点计算的结果由IEEE 754强制要求,因此“0.1 + 0.2 == 0.3”必须在任何兼容系统上评估为假。某些程序依赖于以这种方式运行的浮点运算。
      2017-01-25 17:54:56Z
    3. @ AdamP.Goucher我根据您的评论在2月15日更新了我的帖子。我在这里忽略了评论,所以我现在这样做了。感谢您对答案的改进。
      2017-03-04 18:56:04Z
    4. 醇>

    这里有很多答案可以解释为什么浮点数的运行方式......

    但是很少谈论任意精确度(Pickle提到它)。如果你想要(或需要)精确的精确度,唯一的方法(至少对于有理数)是使用 BC Math 扩展(实际上只是一个 BigNum,任意精度实施......

    添加两个数字:

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

    将导致12345678901235.1234567890 ...

    这称为任意精度数学。基本上所有数字都是为每个操作解析的字符串,并且逐个数字地执行操作(想想长除法,但是由库完成)。所以这意味着它很慢(与常规数学结构相比)。但它非常强大。您可以对任何具有精确字符串表示的数字进行乘法,加法,减法,除法,求模和取幂。

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

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

    使用32位浮点数(双精度)给出估计结果:

     
    2250004.5000023
    

    但是bcmath给出了确切答案:

     
    2250004.50000225
    

    这一切都取决于你需要的精确度。

    此外,还有其他需要注意的地方。 PHP只能表示32位或64位整数(取决于您的安装)。因此,如果一个整数超过native int类型的大小(对于32位为21亿,对于有符号的int为9.2 x10 ^ 18或92亿十亿),PHP会将int转换为float。虽然这不是一个问题(因为所有小于系统浮点精度的int都可以直接表示为浮点数),如果你尝试将两者相乘,它将失去很大的精度。

    例如,鉴于$n = '40000000002'

    作为一个数字,$n将是float(40000000002),这很好,因为它的确切代表。但如果我们对它进行调整,我们得到:float(1.60000000016E+21)

    作为一个字符串(使用BC数学),$n将正好是'40000000002'。如果我们对它进行调整,我们得到:string(22) "1600000000160000000004" ...

    因此,如果您需要具有大数字或有理小数点的精度,您可能需要查看bcmath ......

        
    14
    2010-10-01 18:33:54Z
    1. Nitpick:一个数字,例如1/3,可以有重复的十进制表示,但仍然是理性的。 “有理数”是所有数字,可以表示为两个数字a和b的一部分,其中a和b都是整数。而1/​​3确实是这样一个数字的一​​个例子。
      2012-02-16 07:56:31Z
    2. + 1我来到这里寻找一种方法将一个巨大的字符串除以另一个,并在你的答案中找到bcmath。谢谢!
      2013-10-31 04:19:42Z
    3. 我认为有点近距离地说唯一方法是使用bc_math。我会说推荐的做法是给你bc_math。如果您愿意,您可以自由地实施自己的系统:D它只是比它的价值更麻烦。
      2017-08-25 13:21:16Z
    4. 醇>

    使用PHP的round()功能: http://php.net/manual/en/function.round.php

    这个答案解决了问题,但没有解释原因。我认为很明显[我也是用C ++编程,所以对我来说很明显;]],但如果没有,让我们说PHP有它自己的计算精度,并且在特定情况下它返回了关于该计算的大部分符合信息

        
    2
    2010-09-16 12:58:29Z
    1. - 1因为它绝对不是问题的答案。
      2010-09-16 12:46:10Z
    2. @ Dennis Haarbrink好吧,你贬低了这个,有人低估了我的答案。好吧,那么什么 回答呢?
      2010-09-16 12:49:46Z
    3. @ Andrey:是的,不知道为什么你的答案被低估了,因为它几乎是正确的答案:)最好的答案恕我直言@ircmaxell在评论中OP。
      2010-09-16 12:53:05Z
    4. 这不是OP所要求的。 PS:我没有给你打气。
      2010-09-16 12:56:23Z
    5. @ Tomasz Kowalczyk:嗯,你已经获得了3票和2张票,共计26雷普。我认为这应该足以让你的答案:)
      2010-09-16 13:15:26Z
    6. 醇>

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

     
    <?PHP
    
    $a = '35';
    $b = '-34.99';
    
    echo $a + $b;
    echo '<br />';
    echo bcadd($a,$b,2);
    
    ?>
    

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

    第一行给我0.009999999999998。 第二个给我0.01

        
    2
    2010-09-29 19:49:15Z

    我的php返回0.01 ...

    也许它有php版本的todo,(我使用5.2)

        
    1
    2010-09-16 12:46:03Z
    1. 你是在使用64位版本的PHP 64位机器吗?
    2010-09-16 12:47:21Z
  5. 我也得到0.01,Ubuntu,PHP 5.3.2-1ubuntu4.2。是32位安装。
    2010-09-16 12:51:05Z
  6. 我在RHEL 5.3.2 32位(通过VMWare管理程序的Intel Xeon处理器)上得到0.0099 .... 8。但那是你的浮点......
    2010-09-16 12:54:59Z
  7. 好吧,我认为这至少是评论。写作“在我的机器上工作!”没用。 codinghorror.com/blog/2007/03 /...
    2010-09-16 13:01:05Z
  8. 原因是在某些机器上工作而不是其他机器是因为PHP没有直接分区内存。它调用内置于操作系统中的C库,它通过抽象层与ISA通信并转换为机器指令。假设操作系统遵循IEEE-754浮点标准,您使用的处理器将不会影响输出,但操作系统的抽象层如何解释结果以及C库如何处理结果。 Windows会将任何重复的二进制数舍入为最接近的非重复小数。
    2010-09-29 02:38:57Z
  9. 醇>

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

    我想这不是你想听到的,但它是问题的答案。如何修复看其他答案。

        
    1
    2010-09-16 12:48:24Z
    1. 二进制系列的总和是什么?这不是浮动的存储方式。浮点数基本上是二进制的科学记数法。一位是“符号”(0 =正,1 =负),8位是指数(范围从-128到+127),23位是称为“尾数”的数字。所以(S1)(P8)(M23)的二进制表示具有值(-1 ^ S)M * 2 ^ P
      2010-09-29 02:35:33Z
    2. @ steven_desu谢谢你的教训。这里的关键部分是尾数存储为二进制分数。答案是“为什么”不能精确存储小数部分。
      2010-09-29 09:18:34Z
    3. 醇>

    [解决] 强>

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

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

    看下面的例子:

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

    • 符号:0表示正数,1表示负数,在这种情况下为0。

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

      注意:偏差= 127因此偏差指数= -3 +“偏差”。在单精度中,偏差为127,因此在此示例中,偏差指数为124;

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

      数字1(1.01的第一个位置)不需要保存,因为当以这种方式出现浮动数时,第一个数字总是1。 例如转换:0.11 =&gt; 1.1 * 2 ^( - 1),0.01 =&gt; 1 * 2 ^( - 2)。

    另一个示例show始终删除第一个零:0。1将呈现1 * 2 ^( - 1)。所以第一个alwasy是1。 目前的1 * 2 ^( - 1)数将是:

    • 0:正数
    • 127-1 = 126 = 01111110
    • 分数:00000000000000000000000(23号)

    最后:原始二进制文件是: 0 01111110 00000000000000000000000

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

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

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

     
    0.33333.
    

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

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

    关于这个:

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

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

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

    点击此处: http://www.binaryconvert.com/result_double.html?小数= 048046048049

    因为(01011100001010001111)重复就像1/3一样。因此,计算机无法将此数字保存在内存中。它必须牺牲。这导致计算机不准确。

    高级强> (你必须掌握数学知识) 那么为什么我们可以轻松地以十进制显示0.01而不是二进制。

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

     
    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.
    
        
    1
    2016-08-01 08:09:27Z

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

        
    0
    2013-09-04 15:10:19Z
来源放置 这里