PHP - 浮点数精度

dcm*_*ody 77 php floating-point precision floating-accuracy

$a = '35';
$b = '-34.99';
echo ($a + $b);
Run Code Online (Sandbox Code Playgroud)

结果在0.009999999999998

怎么了?我想知道为什么我的程序会报告奇怪的结果.

为什么PHP没有返回预期的0.01?

Nul*_*ion 120

因为浮点运算!=实数运算.由于不精确性的差异的图示是,对于一些浮标ab,(a+b)-b != a.这适用于使用浮点数的任何语言.

由于浮点是具有有限精度的二进制数,因此存在有限数量的可表示数字,这导致精度问题和这样的惊喜.这是另一个有趣的读物:每个计算机科学家应该知道浮点运算.


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

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

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

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

  • 一个迂腐的说明:有一组有限的浮点数`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`应该是真的(但它是一个有限的集合)...... (7认同)
  • 我会给+1,但链接比我想要的多,解释也少。也许提到二进制的十进制值 0.01 有一个重复的“10100011110101110000”(这个数字看起来像 0.00000010100011110101110000.....)。然后进一步说明,32位计算机仅限于表示23位有效数字(指数加8位,符号加1位=32位),意味着它变成0.00000010100011110101110000101 = d0.0099999979 (2认同)

ste*_*esu 49

与所有数字一样,浮点数必须作为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
Run Code Online (Sandbox Code Playgroud)

打破它的组成部分:

Sign    Power           Mantissa
 0     00000100   10110000000000000000000
 +        +4             1.1011
 +        +4       1 + .5 + .125 + .0625
 +        +4             1.6875
Run Code Online (Sandbox Code Playgroud)

运用我们简单的公式:

(-1^S)M*2^P
(-1^0)(1.6875)*2^(+4)
(1)(1.6875)*(16)
27
Run Code Online (Sandbox Code Playgroud)

换句话说,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
Run Code Online (Sandbox Code Playgroud)

(注意负7是使用2的补码产生的)

应该立即明确,01111100101000111101011100001010看起来不像0.01

但更重要的是,它包含重复小数的截断版本.原始十进制包含重复的"10100011110101110000".我们已将其简化为01000111101011100001010

通过我们的公式将此浮点数转换回十进制,我们得到0.0099999979(请注意,这是针对32位计算机.64位计算机将具有更高的准确性)

十进制等价

如果它有助于更​​好地理解问题,那么在处理重复小数时,让我们看一下十进制科学记数法.

我们假设我们有10个"盒子"来存储数字.因此,如果我们想要存储像1/16这样的数字,我们会写:

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 2 | 5 | 0 | 0 | e | - | 2 |
+---+---+---+---+---+---+---+---+---+---+
Run Code Online (Sandbox Code Playgroud)

这显然只是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 |
+---+---+---+---+---+---+---+---+---+---+
Run Code Online (Sandbox Code Playgroud)

这个新数字0.66667并不完全相同2/3.事实上,它已经过去了0.000003333....如果我们尝试0.66667在3号基础上写作,我们就会得到0.2000000000012...而不是0.2

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

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

+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+
Run Code Online (Sandbox Code Playgroud)

这个数字0.14286是关闭的.000002857...

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

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


irc*_*ell 15

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

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

要添加两个数字:

$number = '12345678901234.1234567890';
$number2 = '1';
echo bcadd($number, $number2);
Run Code Online (Sandbox Code Playgroud)

会导致12345678901235.1234567890......

这称为任意精度数学.基本上所有数字都是为每个操作解析的字符串,并且逐个数字地执行操作(想想长除法,但是由库完成).所以这意味着它很慢(与常规数学结构相比).但它非常强大.您可以对具有精确字符串表示的任何数字进行乘法,加法,减法,除法,求模和取幂.

所以你不能1/3100%准确,因为它有一个重复的小数(因此是不合理的).

但是,如果你想知道1500.0015平方是什么:

使用32位浮点数(双精度)给出估计结果:

2250004.5000023
Run Code Online (Sandbox Code Playgroud)

但是bcmath给出了确切的答案:

2250004.50000225
Run Code Online (Sandbox Code Playgroud)

这一切都取决于您需要的精度.

另外,还有其他需要注意的地方.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 ...

  • Nitpick:一个数字,如1/3,可以有重复的十进制表示,仍然是理性的."有理数"是所有数字,可以表示为两个数字a和b的一部分,其中a和b都是整数.而1/​​3确实是这样一个数字的一​​个例子. (4认同)

Qua*_*kle 5

bcad()在这里可能有用。

<?PHP

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

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

?>
Run Code Online (Sandbox Code Playgroud)

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

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