$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
。这适用于使用浮点数的任何语言。round($result, 2)
来将其四舍五入为2位小数。a
和b
,其中(a+b)-b == a
。它们需要都具有2为质因子,并能够用适当数量的比特表示(单精度约为7位十进制数字,双精度约为16位)。所以a=0.5
和b=0.25
也可以(对于具有32位单精度浮点数系统始终适用)。对于不符合这些先决条件的浮点数,(a+b)-b!=a
。但如果a
和b
都符合这些先决条件,则(a+b)-b==a
应该是正确的(但它是有限集合)... - ircmaxell00000010010110000000000000000000
将其分解为其组件:
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
换句话说,根据IEEE-754标准,00000010010110000000000000000000在浮点数中表示为27。
然而,对于许多数字来说,没有精确的二进制表示方式。就像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's complement表示的)
很明显,01111100101000111101011100001010看起来与0.01完全不同。
更重要的是,这个二进制数包含了一个被截断的重复十进制小数。原来的十进制数包含了一个重复段"10100011110101110000",我们将其简化为01000111101011100001010。
通过我们的公式将这个浮点数转换回十进制,得到0.0099999979(请注意,这是32位计算机的精度。64位计算机会有更高的精度)。
如果想更好地理解问题,我们可以看一下使用科学计数法表示重复小数时的十进制表示。
假设我们有10个“盒子”来存储数字。因此,如果我们想存储1/16这样的数字,我们会写:
+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
+---+---+---+---+---+---+---+---+---+---+
这显然只是6.25e-2
,其中e
是*10^(
的简写。我们为小数分配了4个框,即使我们只需要2个框(用零补齐),并为符号分配了2个框(一个为数字的符号,一个为指数的符号)。-9.9999 e -9
到+9.9999 e +9
的数字。2/3
这样的数字时会发生什么?+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
这个新的数字0.66667
并不完全等于2/3
。实际上,它与2/3
相差0.000003333...
。如果我们试图在三进制中写出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
)的情况。
这里有很多关于浮点数为什么会这样工作的答案...
但是关于任意精度(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或9.2百万亿),PHP将把该整数转换为浮点数。虽然这并不是立即的问题(因为所有小于系统浮点数精度的整数都可以定义为直接可表示为浮点数),但如果您尝试将两个整数相乘,它将失去显着的精度。
例如,给定$n ='40000000002'
:
作为数字,$n
将是float(40000000002)
,这很好因为它被准确表示。但是,如果我们将其平方,我们得到:float(1.60000000016E+21)
使用BC数学函数作为字符串,$n
将完全是'40000000002'
。而且��果我们将其平方,我们得到:string(22) "1600000000160000000004"
...
因此,如果您需要大数字的精度或有理小数点,则可能需要考虑使用bcmath...
bcmath
。谢谢! - mulllhausenbcadd() 在这里可能很有用。
<?PHP
$a = '35';
$b = '-34.99';
echo $a + $b;
echo '<br />';
echo bcadd($a,$b,2);
?>
(为了清晰表达而不高效的输出)
第一行给出0.009999999999998。 第二行给出0.01。
符号位: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 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 (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.
使用PHP的round()
函数:http://php.net/manual/en/function.round.php
这个答案解决了问题,但没有解释为什么。我认为这是显而易见的[我也在C++中编程,所以对我来说很明显;],但如果不是这样的话,那么可以说PHP有它自己的计算精度,在那种特定的情况下,它返回了最符合该计算的信息。
使用number_format(0.009999999999998, 2)
或者$res = $a+$b; -> number_format($res, 2);
会更容易些,不是吗?
$a = 35; $b = -34.99
。 - NullUserException