PHP的浮动数字精度

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

结果为0.009999999999998

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

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

由于浮点运算!=实数运算。 (a+b)-b != a ,对于某些花车ab ,由于不精确性的不同, 这适用于使用浮动的任何语言。

由于浮点数是有限精度的二进制数,所以有一定数量的可表示数 ,这就导致了精度问题和这样的惊喜。 这里还有一个有趣的读法: 每个计算机科学家应该知道什么是浮点运算 。


回到你的问题,基本上没有办法准确地代表34.99或0.01的二进制(就像在十进制,1/3 = 0.3333 …),所以使用近似值。 为了解决这个问题,你可以:

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

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

可惜的是PHP没有像其他 语言那样的十进制数据types。

像所有数字一样,浮点数字必须以0和1的stringforms存储在内存中。 这一切都是电脑。 浮点数与整数的不同之处在于我们如何解释0和1,当我们想看看它们时。

一位是“符号”(0 =正数,1 =负数),8位是指数(范围从-128到+127),23位是已知数字“尾数”(分数)。 所以(S1)(P8)(M23)的二进制表示的值为(-1 ^ S)M * 2 ^ P

“尾数”呈特殊forms。 在正常的科学记数法中,我们显示“一个地方”与分数。 例如:

4.39×10 ^ 2 = 439

在二进制中,“一个地方”是一个单一的位。 由于我们忽略了所有的最左边的0(科学记数法,我们忽略任何微不足道的数字),第一位保证是1

1.101×2 ^ 3 = 1101 = 13

由于我们保证第一位将是1,因此在存储号码时删除此位以节省空间。 所以上面的数字存储为101(尾数)。 假设领先1

作为一个例子,我们来看看二进制string

 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 

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

应该立刻清楚01111100101000111101011100001010看起来不像0.01

但更重要的是,它包含一个重复小数的截断版本。 原来的小数点包含一个重复的“10100011110101110000”。 我们已经简化了这个到01000111101011100001010

通过我们的公式将这个浮点数转换回十进制数,我们得到0.0099999979(注意这是一个32位计算机,一个64位计算机会有更高的精度)

这里有很多关于为什么浮点数的工作方式的答案…

但是没有任何精确的说法(Pickle提到它)。 如果你想(或者需要)精确的精度,唯一的方法就是使用BC Math扩展(这实际上只是一个BigNum,任意精度的实现)。

要添加两个数字:

 $number = '12345678901234.1234567890'; $number2 = '1'; echo bcadd($number, $number2); 

将导致12345678901235.1234567890 … –

这被称为任意精度math。 基本上所有的数字都是为每个操作分析的string,操作是按照数字的方式进行的(思考长分区,但是由图书馆完成)。 所以这意味着它很慢(与常规的math结构相比)。 但它非常强大。 你可以乘,加,减,除,找模和幂的任何数字,具有精确的string表示forms。

所以你不能以100%的精度做1/3 ,因为它有一个重复的小数(因此是不合理的)。

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

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

 2250004.5000023 

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

 2250004.50000225 

这一切都取决于你所需要的精度。

另外,这里还有其他的东西要注意。 PHP只能表示32位或64位整数(取决于你的安装)。 所以如果一个整数超过了本地inttypes的大小(32位为21亿,9.2 x10 ^ 18或92亿十亿),PHP会将int转换为浮点数。 虽然这不是一个问题(因为所有小于系统浮点数的精度都可以直接表示为浮点数),但是如果将两个数相乘,将会失去显着的精度。

例如,给定$n = '40000000002'

作为一个数字, $n将是float(40000000002) ,这是正确的,因为它的确切代表。 但是,如果我们把它平方,我们得到: float(1.60000000016E+21)

作为一个string(使用BCmath), $n将正好是'40000000002' 。 如果我们平方,我们得到: string(22) "1600000000160000000004" … … –

所以,如果你需要大数的精度,或有理数的小数点,你可能想看看bcmath …

使用PHP的round()函数: http : //php.net/manual/en/function.round.php

这个答案解决了问题,但不能解释为什么。 我认为这很明显[我也用C ++进行编程,所以对我来说是显而易见的];但是,如果不是这样的话,让我们说PHP有它自己的计算精度,在这种特殊情况下,它返回了关于该计算的最符合的信息。

我的PHP返回0.01 … 替代文字

也许它有todo与PHP版本,(我使用5.2)

因为0.01不能完全表示为一系列二进制分数的总和。 这就是如何将浮点数存储在内存中。

我想这不是你想听到的,但这是问题的答案。 如何解决看其他答案。

bcadd()在这里可能很有用。

 <?PHP $a = '35'; $b = '-34.99'; echo $a + $b; echo '<br />'; echo bcadd($a,$b,2); ?> 

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

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

[解决]

在这里输入图像描述

每个数字将被保存在计算机中的二进制值,如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)。 所以第一个alwasy是1.目前的数字1 * 2 ^( – 1)将是:

  • 0:正数
  • 127-1 = 126 = 01111110
  • 分数:00000000000000000000000(23位数)

最后:原始二进制是: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(十进制)。 现在让我们用二进制显示这个数字。

 0.01 (decimal) = 0 10001111 01011100001010001111 (01011100001010001111)*(binary) 

请点击此处: http : //www.binaryconvert.com/result_double.html?decimal=048046048049

因为(01011100001010001111)就像1/3一样重复。 所以电脑无法将这个号码保存在内存中。 它必须牺牲。 这导致电脑不准确。

高级 (你必须有math的知识)为什么我们可以很容易地显示在十进制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. 

使用number_format(0.009999999999998, 2)$res = $a+$b; -> number_format($res, 2);是不是更容易些$res = $a+$b; -> number_format($res, 2); $res = $a+$b; -> number_format($res, 2);