PHP - 浮点数精度

时间:2010-09-16 12:40:00

标签: php floating-point precision floating-accuracy

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

结果为0.009999999999998

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

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

9 个答案:

答案 0 :(得分:117)

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

由于floating point是具有有限精度的二进制数,因此representable numbers的数量有限,导致accuracy problems和这样的惊喜。这是另一个有趣的读物:What Every Computer Scientist Should Know About Floating-Point Arithmetic


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

  1. 在结果上使用round($result, 2)将其四舍五入到小数点后两位。

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

  3. 遗憾的是,PHP没有像other languages那样的十进制数据类型。

答案 1 :(得分:47)

浮点数与所有数字一样,必须以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...的关闭。如果我们尝试在基数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相对)

这些小的小数差异看起来应该很熟悉

答案 2 :(得分:14)

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

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

添加两个数字:

$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 ......

答案 3 :(得分:2)

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

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

答案 4 :(得分:2)

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

<?PHP

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

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

?>

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

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

答案 5 :(得分:1)

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

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

答案 6 :(得分:1)

我的php返回0.01 ... alt text

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

答案 7 :(得分:1)

<强> [解决]

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。

  • 指数: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)

另一个示例显示总是删除第一个零: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?decimal=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?decimal=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.

答案 8 :(得分:0)

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