PHP - 浮点数精度
$a = '35';
$b = '-34.99';
echo ($a + $b);
结果在 0.009999999999998
这是怎么回事?我想知道为什么我的程序不断报告奇怪的结果。
为什么 PHP 不返回预期的 0.01?
$a = '35';
$b = '-34.99';
echo ($a + $b);
结果在 0.009999999999998
这是怎么回事?我想知道为什么我的程序不断报告奇怪的结果。
为什么 PHP 不返回预期的 0.01?
因为浮点算术!=实数算术。由于不精确而导致的差异的例证是,对于某些浮点数和 ,。这适用于使用浮点数的任何语言。a
b
(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
例如,让我们以二进制字符串为例
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 -2
e
*10^(
使用像这样的10个框,我们可以显示从到-9.9999 e -9
+9.9999 e +9
这适用于具有4位或更少小数位的任何内容,但是当我们尝试存储诸如?2/3
+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
这个新数字并不完全等于 。事实上,它已经关闭了。如果我们尝试以3为基数编写,我们将得到0.2000000000012...而不是
0.66667
2/3
0.000003333...
0.66667
0.2
如果我们采用具有较大重复小数的东西,例如.,则此问题可能会变得更加明显。这有6个重复的数字:1/7
0.142857142857...
将其存储到我们的十进制计算机中,我们只能显示其中的5个数字:
+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
此数字 , 由0.14286
.000002857...
它“接近正确”,但它并不完全正确,所以如果我们试图以7为基数写这个数字,我们会得到一些可怕的数字而不是。事实上,将它插入 Wolfram Alpha 会得到:.10000022320335...
0.1
这些微小的分数差异应该看起来很熟悉(而不是0.0099999979
0.01
)