PHP - 浮点数精度

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

结果在 0.009999999999998

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

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


答案 1

因为浮点算术!=实数算术。由于不精确而导致的差异的例证是,对于某些浮点数和 ,。这适用于使用浮点数的任何语言。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。

遗憾的是,PHP不像其他语言那样具有十进制数据类型。


答案 2

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

这显然只是 ,哪里是 的简写。我们为小数分配了 4 个框,即使我们只需要 2 个(带零的填充),也为符号分配了 2 个框(一个用于数字的符号,一个是指数的符号)6.25 e -2e*10^(

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

这适用于具有4位或更少小数位的任何内容,但是当我们尝试存储诸如?2/3

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

这个新数字并不完全等于 。事实上,它已经关闭了。如果我们尝试以3为基数编写,我们将得到0.2000000000012...而不是0.666672/30.000003333...0.666670.2

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

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

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

此数字 , 由0.14286.000002857...

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

这些微小的分数差异应该看起来很熟悉(而不是0.00999999790.01)


推荐