考虑以下代码:
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
为什么会出现这些错误?
当前回答
这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用正常人能够理解的方式来解决这个问题。
想象一下,你正试图把披萨切成薄片。你有一个机器人披萨切割机,可以将披萨切成两半。它可以将整个披萨减半,也可以将现有的披萨减半,但无论如何,减半总是准确的。
那台披萨切割机动作非常精细,如果你从一整块披萨开始,然后将其减半,然后继续每次将最小的披萨片减半,你可以在披萨片太小甚至无法实现高精度功能之前,将其减半53次。此时,您不能再将非常薄的切片减半,但必须按原样包含或排除它。
现在,你如何将所有的切片以这样一种方式分割,使其达到披萨的十分之一(0.1)或五分之一(0.2)?真的想一想,试着解决它。如果你手边有一个神话般的精密披萨切割机,你甚至可以尝试使用真正的披萨
当然,大多数有经验的程序员都知道真正的答案,那就是,无论你切得多细,都无法用这些切片拼凑出十分之一或五分之一的披萨。你可以做一个非常好的近似值,如果你把0.1的近似值和0.2的近似值相加,你会得到非常好的0.3的近似值。
对于双精度数字(允许您将披萨减半53次的精度),小于或大于0.1的数字分别为0.09999999999999999167332731531132594682276248931884765625和0.1000000000000000055511151231257827021181583404541015625。后者比前者更接近0.1,因此,如果输入值为0.1,数字解析器将倾向于后者。
(这两个数字之间的区别是“最小切片”,我们必须决定是否包含,这会引入向上的偏差,或者排除,这会带来向下的偏差。最小切片的技术术语是ulp。)
在0.2的情况下,数字都是相同的,只是放大了2倍。同样,我们赞成略高于0.2的值。
注意,在这两种情况下,0.1和0.2的近似值都有轻微的向上偏差。如果我们加上足够多的这些偏差,它们会将数字推离我们想要的越来越远,事实上,在0.1+0.2的情况下,偏差足够高,从而导致的数字不再是最接近0.3的数字。
特别是,0.1+0.2实际上是0.1000000000000000055511151231257827021181583404541015625+0.0200000000000000011102230246251565404236316680908203125=0.30000000000000000444089209850062616169452667236328125,而最接近0.3的数字实际上是0.29999999999988897769753748434595763683319091796875。
另外,一些编程语言还提供了披萨切割机,可以将披萨切成十分之一。虽然这种披萨切刀并不常见,但如果你有机会切到一个,那么你应该在切到十分之一或五分之一的披萨片非常重要的时候使用它。
(最初发布在Quora上。)
其他回答
二进制浮点数学是这样的。在大多数编程语言中,它基于IEEE 754标准。问题的关键在于,数字以这种格式表示为整数乘以2的幂;分母不是2的幂的有理数(如0.1,即1/10)无法精确表示。
对于标准binary64格式的0.1,表示形式可以完全写为
0.1000000000000000055511151231257827021181583404541015625(十进制),或0x1.999999999999ap-4,采用C99六进制浮点数表示法。
相比之下,有理数0.1(1/10)可以完全写成
0.1(十进制),或0x1.999999999999999…p-4,类似于C99十六进制浮点数,其中。。。表示9的无限序列。
程序中的常数0.2和0.3也将近似于其真实值。恰好最接近0.2的两倍大于有理数0.2,但最接近0.3的两倍小于有理数0.3。0.1和0.2的和最终大于有理数0.3,因此与代码中的常数不一致。
浮点运算问题的一个相当全面的处理是每个计算机科学家都应该知道的浮点运算。有关更容易理解的解释,请参阅floatingpoint-gui.de。
边注:所有位置(以N为基数)数字系统都有精度问题
普通的十进制(以10为基数)数字也有同样的问题,这就是为什么像1/3这样的数字最终会变成0.33333333。。。
您刚刚偶然发现了一个数字(3/10),它很容易用十进制表示,但不适合二进制。它也是双向的(在某种程度上):1/16在十进制中是一个丑陋的数字(0.0625),但在二进制中,它看起来和十进制中的第10000个一样整洁(0.0001)**-如果我们在日常生活中习惯使用基数为2的数字系统,你甚至会看着这个数字,本能地理解你可以通过将某个数字减半,一次又一次地减半来达到这个目的。
当然,这并不是浮点数在内存中的存储方式(它们使用了一种科学的表示法)。然而,它确实说明了一点,二进制浮点精度错误往往会出现,因为我们通常感兴趣的“真实世界”数字往往是十的幂,但这只是因为我们每天使用十进制数字系统。这也是为什么我们会说71%而不是“每7取5”(71%是一个近似值,因为5/7不能用任何小数精确表示)。
所以不:二进制浮点数并没有被破坏,它们只是碰巧和其他N进制一样不完美:)
边注:在编程中使用浮点
实际上,这种精度问题意味着在显示浮点数之前,需要使用舍入函数将浮点数舍入到您感兴趣的小数位数。
您还需要用允许一定公差的比较来替换相等测试,这意味着:
如果(x==y){…}则不执行
相反,如果(abs(x-y)<myToleranceValue){…},则执行此操作。
其中abs是绝对值。需要为您的特定应用程序选择myToleranceValue,这与您准备允许多少“摆动空间”以及您将要比较的最大值(由于精度损失问题)有很大关系。当心您选择的语言中的“epsilon”样式常量。这些值可以用作公差值,但它们的有效性取决于您使用的数字的大小,因为使用大数字的计算可能会超过epsilon阈值。
已经发布了很多好的答案,但我想再补充一个。
并非所有数字都可以通过浮点数/双精度表示例如,在IEEE754浮点标准中,数字“0.2”将以单精度表示为“0.200000003”。
用于在引擎盖下存储实数的模型将浮点数表示为
即使您可以轻松键入0.2,FLT_RADIX和DBL_RADIX都是2;对于使用“IEEE二进制浮点运算标准(ISO/IEC Std 754-1985)”的带有FPU的计算机,不是10。
所以准确地表示这些数字有点困难。即使在没有任何中间计算的情况下显式指定此变量。
鉴于没有人提到这一点。。。
一些高级语言(如Python和Java)提供了克服二进制浮点限制的工具。例如:
Python的十进制模块和Java的BigDecimal类,它们在内部使用十进制表示法(与二进制表示法相反)表示数字。两者都有有限的精度,因此它们仍然容易出错,但它们解决了二进制浮点运算中最常见的问题。小数在处理金钱时很好:10美分加20美分总是正好是30美分:>>> 0.1 + 0.2 == 0.3错误>>>十进制('0.1')+十进制('0.2')==十进制('0.3')真的Python的十进制模块基于IEEE标准854-1987。Python的分数模块和Apache Common的BigFraction类。两者都将有理数表示为(分子、分母)对,它们可能给出比十进制浮点运算更精确的结果。
这两种解决方案都不是完美的(特别是如果我们考虑性能,或者如果我们需要非常高的精度),但它们仍然解决了二进制浮点运算的大量问题。
为了好玩,我按照标准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。
正常的算术是以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) ...