提问人:dcmoody 提问时间:9/16/2010 最后编辑:Joe DFdcmoody 更新时间:1/1/2020 访问量:60269
PHP - 浮点数精度 [重复]
PHP - Floating Number Precision [duplicate]
问:
$a = '35';
$b = '-34.99';
echo ($a + $b);
结果为 0.009999999999998
这是怎么回事?我想知道为什么我的程序总是报告奇怪的结果。
为什么 PHP 不返回预期的 0.01?
答:
使用PHP的功能:http://php.net/manual/en/function.round.phpround()
这个答案解决了问题,但没有解释为什么。我认为这是显而易见的[我也在用 C++ 编程,所以这对我来说是显而易见的;]],但如果不是,假设 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。
评论
a
b
(a+b)-b == a
a = 0.5
b = 0.25
(a+b)-b != a
a
b
(a+b)-b == a
与所有数字一样,浮点数必须以 0 和 1 的字符串形式存储在内存中。这都是计算机的比特。浮点数与整数的不同之处在于,当我们想要查看 0 和 1 时,我们如何解释它们。
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.000000100011110101110000.....带有重复的“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.00999999979(请注意,这适用于 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.20000000000012...,
而不是0.66667
2/3
0.000003333...
0.66667
0.2
如果我们采用具有较大重复小数点的东西,这个问题可能会变得更加明显,例如 .这有 6 个重复的数字:1/7
0.142857142857...
将其存储到我们的十进制计算机中,我们只能显示以下数字中的 5 位:
+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
这个数字 , 是 off by0.14286
.000002857...
它“接近正确”,但它并不完全正确,因此,如果我们尝试将这个数字写在以 7 为基数的基数中,我们会得到一些可怕的数字而不是 .事实上,将其插入 Wolfram Alpha 我们得到:.10000022320335...
0.1
这些微小的分数差异对你来说应该很熟悉(而不是0.0099999979
0.01
)
评论
bcadd() 在这里可能很有用。
<?PHP
$a = '35';
$b = '-34.99';
echo $a + $b;
echo '<br />';
echo bcadd($a,$b,2);
?>
(为清晰起见,输出效率低下)
第一行给了我 0.0099999999999998。 第二个给了我 0.01
这里有很多关于为什么浮点数会以这种方式工作的答案......
但是很少有人谈论任意的精确度(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 位整数(取决于您的安装)。因此,如果整数超过原生 int 类型的大小(32 位为 21 亿,9.2 x10^18 或有符号整数为 92 亿),PHP 会将 int 转换为浮点数。虽然这并不是一个直接的问题(因为根据定义,所有小于系统浮点数精度的整数都可以直接表示为浮点数),但如果你尝试将两个相乘,它将失去显着的精度。
例如,给定:$n = '40000000002'
作为一个数字,将是 ,这很好,因为它被精确地表示出来。但是,如果我们将其平方,我们会得到:$n
float(40000000002)
float(1.60000000016E+21)
作为一个字符串(使用 BC 数学),将正好是 .如果我们把它平方,我们会得到:......$n
'40000000002'
string(22) "1600000000160000000004"
因此,如果您需要大数或有理小数点的精度,您可能需要研究 bcmath...
评论
bcmath
不是更容易使用还是?number_format(0.009999999999998, 2)
$res = $a+$b; -> number_format($res, 2);
每个数字都将以二进制值(例如 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 => 1.1*2^(-1),0.01 => 1*2^(-2)。
另一个示例显示始终删除第一个零:0.1 将显示 1*2^(-1)。所以第一个总是 1。 当前 1*2^(-1) 的数为:
- 0:正数
- 127-1 = 126 = 01111110
- 分数: 0000000000000000000000000 (23 个数字)
最后:原始二进制文件是: 0 01111110 00000000000000000000000
在这里查看: http://www.binaryconvert.com/result_float.html?decimal=048046053
现在,如果您已经了解如何保存浮点数。如果数字不能以 32 位(简单精度)保存,会发生什么情况。
例如:十进制。1/3 = 0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333再重复一遍,这不是真的。试想一下。因此,保存在计算机中的数据将是:
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.
评论
$a = 35; $b = -34.99