PHP - 浮点数精度 [重复]

PHP - Floating Number Precision [duplicate]

提问人:dcmoody 提问时间:9/16/2010 最后编辑:Joe DFdcmoody 更新时间:1/1/2020 访问量:60269

问:

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

结果为 0.009999999999998

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

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

php 浮点 精度 float-accuracy

评论

17赞 ircmaxell 9/16/2010
我建议阅读有关浮点数的信息。具体来说,“可表示数字、转换和四舍五入”和“准确性问题”部分。如果您想了解它们的工作原理,本文的其余部分很好,但这两个部分特别适用于您的问题......
10赞 NullUserException 9/16/2010
还值得注意的是,您使用的是字符串而不是数字(它们被隐式转换,但仍然如此)。而是去做。$a = 35; $b = -34.99
0赞 Selay 9/4/2018
检查:stackoverflow.com/questions/17210787/... 和 stackoverflow.com/questions/588004/...

答:

0赞 Tomasz Kowalczyk 9/16/2010 #1

使用PHP的功能:http://php.net/manual/en/function.round.phpround()

这个答案解决了问题,但没有解释为什么。我认为这是显而易见的[我也在用 C++ 编程,所以这对我来说是显而易见的;]],但如果不是,假设 PHP 有自己的计算精度,并且在那种特定情况下它返回了有关该计算的最合规信息。

评论

7赞 Dennis Haarbrink 9/16/2010
-1 因为它绝对不是问题的答案。
0赞 Andrey 9/16/2010
@Dennis哈布林克,好吧,你投了反对票,有人反对我的回答。那么,答案是什么呢?
0赞 Dennis Haarbrink 9/16/2010
@Andrey:是的,不知道为什么你的答案被否决了,因为它几乎是正确的答案:)恕我直言,最好的答案是在 OP 的评论中@ircmaxell。
0赞 NullUserException 9/16/2010
这不是OP所要求的。PS:我没有投反对票。
1赞 Dennis Haarbrink 9/16/2010
@Tomasz Kowalczyk:嗯,你已经收到了 3 张赞成票和 2 张反对票,总共 26 张。我想这应该足以让你回答:)
3赞 Andrey 9/16/2010 #2

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

我想这不是你想听到的,但它是对问题的回答。有关如何修复,请参阅其他答案。

评论

0赞 stevendesu 9/29/2010
二进制 what-now 的系列之和?浮点数不是这样存储的。浮点数本质上是二进制的科学记数法。1 位是“符号”(0 = 正数,1 = 负数),8 位是指数(范围从 -128 到 +127),23 位是称为“尾数”的数字。所以 (S1)(P8)(M23) 的二进制表示具有值 (-1^S)M*2^P
0赞 Andrey 9/29/2010
@steven_desu谢谢你的教训。这里的关键部分是尾数存储为二进制分数。这是对“为什么”不能精确存储十进制分数的问题的回答。
136赞 NullUserException 9/16/2010 #3

因为浮点运算 != 实数算术。由于不精确而导致的差异的例证是,对于某些浮点数和 、 。这适用于任何使用浮点数的语言。ab(a+b)-b != a

由于浮点数是精度有限的二进制数,因此可表示的数字数量有限,这会导致准确性问题和意外。这是另一个有趣的读物:每个计算机科学家都应该知道的关于浮点运算的知识


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

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

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

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

评论

0赞 Andrey 9/16/2010
我要补充一点,0.01 也不能按原样表示。这应该被标记为正确,因为它给出了解释和如何修复。但是为了增加它的实用性,请解释一下为什么 fp != real,因为所有二进制的东西和精度都丢失了
0赞 NullUserException 9/16/2010
@irc谢谢。我在答案中加入了你的一些评论
8赞 ircmaxell 9/16/2010
一个迂腐的注释:有一组有限的浮点数,其中.它们只需要具有 2 的质因数,并且可以用适当的位数表示(单精度约为 7 位十进制,双精度约为 16 位)。因此,并且有效(并且将始终适用于具有 32 位单精度浮点数的系统)。对于不满足其中任一或两个前提条件的浮点数,则 .但是,如果两者都符合这些前提条件,那么应该是真的(但它是一个有限的集合)......ab(a+b)-b == aa = 0.5b = 0.25(a+b)-b != aab(a+b)-b == a
1赞 NullUserException 9/16/2010
@irc 正确;我在那里用错了词。
2赞 stevendesu 9/29/2010
我会给+1,但链接比我想要的要多,解释也少。也许提到二进制中的十进制值 0.01 有一个重复的“10100011110101110000”(该数字看起来像 0.000000100011110101110000.....)。然后进一步解释,一台 32 位计算机只能表达 23 位有效数字(加上 8 表示指数,1 表示符号 = 32 位),这意味着它变为 0.0000001010001110110110000101 = d0.0099999979
62赞 stevendesu 9/29/2010 #4

与所有数字一样,浮点数必须以 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 -2e*10^(

使用这样的 10 个框,我们可以显示从 到-9.9999 e -9+9.9999 e +9

这适用于小数点后 4 位或更少的任何内容,但是当我们尝试存储这样的数字时会发生什么?2/3

+---+---+---+---+---+---+---+---+---+---+
| + | 6 | . | 6 | 6 | 6 | 7 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

这个新数字并不完全等于 .事实上,它已经关闭了.如果我们尝试以 3 为基数编写,我们将得到 0.20000000000012...,而不是0.666672/30.000003333...0.666670.2

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

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

+---+---+---+---+---+---+---+---+---+---+
| + | 1 | . | 4 | 2 | 8 | 6 | e | - | 1 |
+---+---+---+---+---+---+---+---+---+---+

这个数字 , 是 off by0.14286.000002857...

它“接近正确”,但它并不完全正确,因此,如果我们尝试将这个数字写在以 7 为基数的基数中,我们会得到一些可怕的数字而不是 .事实上,将其插入 Wolfram Alpha 我们得到:.10000022320335...0.1

这些微小的分数差异对你来说应该很熟悉(而不是0.00999999790.01)

评论

1赞 NikiC 9/30/2010
+1 谢谢,现在我知道浮子是如何存储的了。PS:不,Windows没有。至少在 PHP5.3.1/Win7 中,我确实遇到了浮点问题;)
1赞 Adam P. Goucher 1/26/2017
应删除最后一段(声称操作系统决定是否舍入浮点值)。浮点计算的结果由 IEEE 754 强制执行,因此在任何兼容系统上,“0.1 + 0.2 == 0.3”的计算结果都必须为 false。某些程序依赖于以这种方式运行的浮点运算。
1赞 stevendesu 3/5/2017
@AdamP.Goucher 我根据您的评论于 2 月 15 日更新了我的帖子。我忽略了在这里发表评论,所以我现在正在这样做。感谢您对答案的改进。
5赞 Quasipickle 9/30/2010 #5

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

<?PHP

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

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

?>

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

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

18赞 ircmaxell 10/2/2010 #6

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

但是很少有人谈论任意的精确度(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'

作为一个数字,将是 ,这很好,因为它被精确地表示出来。但是,如果我们将其平方,我们会得到:$nfloat(40000000002)float(1.60000000016E+21)

作为一个字符串(使用 BC 数学),将正好是 .如果我们把它平方,我们会得到:......$n'40000000002'string(22) "1600000000160000000004"

因此,如果您需要大数或有理小数点的精度,您可能需要研究 bcmath...

评论

4赞 Mikko Rantalainen 2/16/2012
吹毛求疵:一个数字,如 1/3,可以有重复的十进制表示,并且仍然是理性的。“有理数”是所有可以表示为两个数字 a 和 b 的分数的数字,其中 a 和 b 都是整数。而 1/3 确实是这样一个数字的一个例子。
1赞 mulllhausen 10/31/2013
+1 我来这里寻找一种将一根巨大的绳子除以另一根的方法,并在您的答案中找到。谢谢!bcmath
0赞 stevendesu 8/25/2017
我认为说唯一的方法是使用bc_math有点狭隘。我想说的是,推荐的方法是bc_math给你。如果您愿意,您可以自由地实现自己的系统:D这比它的价值更麻烦。
0赞 Jurijs Nesterovs 9/4/2013 #7

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

2赞 christian Nguyen 7/30/2016 #8

每个数字都将以二进制值(例如 0、1)保存在计算机中。在单精度中,数字占用 32 位。

浮点数可以用以下方式表示:1 位表示符号,8 位表示指数,23 位称为尾数(分数)。

请看下面的例子:

0.15625 = 0.00101 = 1.01*2^(-3)

enter image description here

  • 符号: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.