考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些错误?
考虑以下代码:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
为什么会出现这些错误?
当前回答
想象一下,以10为基数,例如8位数的精度工作。您检查是否
1/3 + 2 / 3 == 1
并了解到这返回错误。为什么?好吧,作为真实的数字
1/3=0.333….和2/3=0.666。。。。
在小数点后八位截断,我们得到
0.33333333 + 0.66666666 = 0.99999999
当然,这与1.00000000正好相差0.00000001。
具有固定位数的二进制数的情况完全类似。作为实数,我们有
1/10=0.0001100110011001100…(底座2)
and
1/5=0.00111001100110011001…(底座2)
如果我们把这些截成七位
0.0001100 + 0.0011001 = 0.0100101
而另一方面,
3/10=0.010011001100110011…(基数2)
被截断为七位的值为0.0100110,两者相差0.0000001。
确切的情况稍显微妙,因为这些数字通常以科学符号存储。因此,例如,我们可以将其存储为1.10011*2^-4,而不是将1/10存储为0.0001100,这取决于我们为指数和尾数分配了多少位。这会影响计算的精度位数。
结果是,由于这些舍入错误,您根本不想在浮点数上使用==。相反,您可以检查它们的差值的绝对值是否小于某个固定的小数字。
其他回答
为了好玩,我按照标准C99的定义玩了浮点数的表示,并编写了下面的代码。
代码以3个独立的组打印浮点的二进制表示
SIGN EXPONENT FRACTION
然后,它打印一个和,当以足够的精度求和时,它将显示硬件中真正存在的值。
因此,当你写float x=999…时,编译器会将该数字转换为函数xx打印的位表示,这样函数yy打印的和就等于给定的数字。
事实上,这个总数只是一个近似值。对于数字999999999,编译器将在浮点的位表示中插入数字1000000000
代码之后,我附加了一个控制台会话,在该会话中,我计算硬件中真正存在的两个常量(减去PI和999999999)的项和,并由编译器插入其中。
#include <stdio.h>
#include <limits.h>
void
xx(float *x)
{
unsigned char i = sizeof(*x)*CHAR_BIT-1;
do {
switch (i) {
case 31:
printf("sign:");
break;
case 30:
printf("exponent:");
break;
case 23:
printf("fraction:");
break;
}
char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
printf("%d ", b);
} while (i--);
printf("\n");
}
void
yy(float a)
{
int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
int fraction = ((1<<23)-1)&(*(int*)&a);
int exponent = (255&((*(int*)&a)>>23))-127;
printf(sign?"positive" " ( 1+":"negative" " ( 1+");
unsigned int i = 1<<22;
unsigned int j = 1;
do {
char b=(fraction&i)!=0;
b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
} while (j++, i>>=1);
printf("*2^%d", exponent);
printf("\n");
}
void
main()
{
float x=-3.14;
float y=999999999;
printf("%lu\n", sizeof(x));
xx(&x);
xx(&y);
yy(x);
yy(y);
}
这里是一个控制台会话,我在其中计算硬件中存在的浮点值的实际值。我使用bc打印主程序输出的项的总和。可以将该和插入python-repl或类似的内容中。
-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872
就是这样。999999999的值实际上是
999999999.999999446351872
您也可以通过bc检查-3.14也受到干扰。不要忘记在bc中设置比例因子。
显示的金额是硬件内部的金额。通过计算它获得的值取决于设置的比例。我确实将比例因子设置为15。数学上,以无限的精度,它似乎是1000000000。
由于这篇文章对当前的浮点实现进行了一般性的讨论,我想补充一下,有一些项目正在解决它们的问题。
看看https://posithub.org/例如,它展示了一种称为posit(及其前身unum)的数字类型,它承诺以更少的比特提供更好的精度。如果我的理解是正确的,它也解决了问题中的问题。非常有趣的项目,背后的人是数学家约翰·古斯塔夫森博士。整个过程都是开源的,用C/C++、Python、Julia和C#实现了许多实际的实现(https://hastlayer.com/arithmetics).
正常的算术是以10为基数的,所以小数表示十分、百分等。当你试图用二进制2为基数的算术表示浮点数时,你要处理的是半、四、八等。
在硬件中,浮点存储为整数尾数和指数。尾数表示有效数字。指数类似于科学记数法,但它使用的基数是2而不是10。例如,64.0将用尾数1和指数6表示。0.125将用尾数1和指数-3表示。
浮点小数必须加上2的负幂
0.1b = 0.5d
0.01b = 0.25d
0.001b = 0.125d
0.0001b = 0.0625d
0.00001b = 0.03125d
等等
在处理浮点运算时,通常使用误差增量而不是相等运算符。而不是
if(a==b) ...
你会使用
delta = 0.0001; // or some arbitrarily small amount
if(a - b > -delta && a - b < delta) ...
浮点数的陷阱是它们看起来像十进制,但它们是二进制的。
2的唯一素因子是2,而10的素因子为2和5。这样做的结果是,每一个可以完全写成二进制分数的数字也可以完全写成十进制分数,但只有一部分可以写成十进制分数的数字可以写成二进制分数。
浮点数本质上是一个有效位数有限的二进制分数。如果你超过这些有效数字,那么结果将被四舍五入。
当您在代码中键入文字或调用函数将浮点数解析为字符串时,它需要一个十进制数,并将该十进制数的二进制近似值存储在变量中。
当您打印浮点数或调用函数将浮点数转换为字符串时,它将打印浮点数的十进制近似值。可以将二进制数字精确地转换为十进制,但在转换为字符串*时,我所知道的任何语言都不会默认这样做。一些语言使用固定数量的有效数字,其他语言使用最短的字符串,该字符串将“往返”返回到相同的浮点值。
*Python在将浮点数转换为“decimal.decimal”时确实会进行精确的转换。这是我所知道的获得浮点数的精确十进制等效值的最简单方法。
我的解决方法:
function add(a, b, precision) {
var x = Math.pow(10, precision || 2);
return (Math.round(a * x) + Math.round(b * x)) / x;
}
精度是指在加法过程中要保留小数点后的位数。