考虑以下代码:
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上。)
其他回答
硬件设计师的视角
我认为,既然我设计并构建了浮点硬件,我就应该添加一个硬件设计师的视角。了解错误的来源可能有助于了解软件中发生的情况,最终,我希望这有助于解释为什么浮点错误会发生并似乎会随着时间累积的原因。
1.概述
从工程角度来看,大多数浮点运算都会有一些误差,因为进行浮点运算的硬件只需要在最后一个位置的误差小于一个单位的一半。因此,许多硬件将停止在一个精度上,该精度只需要在单个操作的最后位置产生小于一个单位的一半的误差,这在浮点除法中尤其有问题。什么构成一个操作取决于该单元需要多少个操作数。大多数情况下,它是两个,但有些单位需要3个或更多操作数。因此,不能保证重复操作会导致期望的错误,因为错误会随着时间的推移而增加。
2.标准
大多数处理器遵循IEEE-754标准,但有些处理器使用非规范化或不同的标准例如,IEEE-754中存在一种非规范化模式,该模式允许以精度为代价表示非常小的浮点数。然而,下面将介绍IEEE-754的标准化模式,这是典型的操作模式。
在IEEE-754标准中,硬件设计者可以使用误差/ε的任何值,只要它在最后一个位置小于一个单位的一半,并且一次操作的结果只需要在最后一位小于一个单元的一半。这解释了为什么当重复操作时,错误会增加。对于IEEE-754双精度,这是第54位,因为53位用于表示浮点数的数字部分(标准化),也称为尾数(例如5.3e5中的5.3)。下一节将更详细地介绍各种浮点操作的硬件错误原因。
3.除法舍入误差的原因
浮点除法误差的主要原因是用于计算商的除法算法。大多数计算机系统使用逆函数的乘法来计算除法,主要是Z=X/Y,Z=X*(1/Y)。迭代地计算除法,即每个周期计算商的一些比特,直到达到所需的精度,对于IEEE-754来说,这是最后一位误差小于一个单位的任何值。Y(1/Y)的倒数表在慢除法中被称为商选择表(QST),商选择表的位大小通常是基数的宽度,或每次迭代中计算的商的位数,加上几个保护位。对于IEEE-754标准,双精度(64位),它将是除法器基数的大小,加上几个保护位k,其中k>=2。因此,例如,一次计算2位商(基数4)的除法器的典型商选择表将是2+2=4位(加上几个可选位)。
3.1除法舍入误差:倒数近似
商选择表中的倒数取决于除法:慢除法如SRT除法,或快除法如Goldschmidt除法;根据除法算法修改每个条目,以尝试产生最小的可能误差。然而,在任何情况下,所有的倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都迭代地计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于最后一个单位的一半。慢除法计算每一步的商的固定位数,通常构建成本较低,而快除法计算每步的位数可变,构建成本通常较高。除法中最重要的部分是,它们大多依赖于通过倒数的近似值进行重复乘法,因此容易出错。
4.其他操作中的舍入错误:截断
所有操作中舍入误差的另一个原因是IEEE-754允许的最终答案的不同截断模式。有截断、向零舍入、向最接近(默认)舍入、向下舍入和向上舍入。所有方法都会在单个操作的最后位置引入小于一个单位的误差元素。随着时间的推移和重复操作,截断也会累积地增加结果误差。这种截断误差在涉及某种形式的重复乘法的求幂运算中尤其有问题。
5.重复操作
由于执行浮点计算的硬件只需要在单个操作的最后一个位置产生误差小于一个单位的一半的结果,因此如果不注意,误差将随着重复操作而增加。这就是为什么在需要有界误差的计算中,数学家使用诸如在IEEE-754的最后一位使用舍入到最接近的偶数位的方法,因为随着时间的推移,误差更可能相互抵消,而区间算术结合了IEEE754舍入模式的变化来预测舍入误差,并对其进行校正。由于与其他舍入模式相比,其相对误差较低,因此舍入到最近的偶数位(最后一位)是IEEE-754的默认舍入模式。
请注意,默认舍入模式(舍入到最后一位最接近的偶数位)保证一次操作的误差小于最后一位单位的一半。仅使用截断、向上舍入和向下舍入可能会导致误差大于最后一位一个单位的一半,但小于最后一位的一个单位,因此不建议使用这些模式,除非它们用于区间算术。
6.总结
简而言之,浮点运算中出现错误的根本原因是硬件中的截断和除法中倒数的截断。由于IEEE-754标准只要求单个操作的误差小于最后一位一个单位的一半,因此重复操作中的浮点误差将相加,除非得到纠正。
这个问题的许多重复问题都是关于浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果而不是仅仅阅读它,更容易了解它的工作原理。一些语言提供了实现这一点的方法,例如在Java中将浮点或双精度转换为BigDecimal。
由于这是一个语言不可知的问题,因此需要语言不可知工具,例如十进制到浮点转换器。
将其应用于问题中的数字,视为双精度:
0.1转换为0.1000000000000000055511151231257827021181583404541015625,
0.2转换为0.200000000000000011102230246251565404236316680908203125,
0.3转换为0.299999999999999988897769753748434595763683319091796875,以及
0.300000000000000004转换为0.30000000000000000444089209850062616169452667236328125。
手动或在十进制计算器(如Full Precision calculator)中添加前两个数字,显示实际输入的精确和为0.30000000000000000166533453693773481063544750213623046875。
如果四舍五入到等于0.3,则舍入误差将为0.000000000000000027755575615628913510591702705078125。四舍五入等于0.300000000000000004也会产生舍入误差0.000000000000000027755575615628913510591702705078125。打成平手的规则适用。
返回浮点转换器,0.300000000000000004的原始十六进制是3fd333333333334,以偶数结尾,因此是正确的结果。
其实很简单。当你有一个基数为10的系统(像我们的系统)时,它只能表示使用基数素因子的分数。10的主要因子是2和5。因此,1/2、1/4、1/5、1/8和1/10都可以清晰地表达,因为分母都使用10的素因子。相比之下,1/3、1/6和1/7都是重复小数,因为它们的分母使用3或7的素因子。在二进制(或基数2)中,唯一的素因子是2。所以你只能清楚地表达分数,它只包含2作为素因子。在二进制中,1/2、1/4、1/8都可以清晰地表示为小数。而1/5或1/10将是重复小数。因此,0.1和0.2(1/10和1/5)虽然在以10为基数的系统中是干净的小数,但在计算机运行的以2为基数的体系中是重复的小数。当你对这些重复的小数进行数学运算时,当你将计算机的以2(二进制)为基数的数字转换为更易于人类阅读的以10为基础的数字时,你最终会留下剩余部分。
从…起https://0.30000000000000004.com/
另一种方法是:使用64位来表示数字。因此,无法精确表示超过2**64=18446744073709551616个不同的数字。
然而,Math表示,在0和1之间已经有无限多的小数。IEE 754定义了一种编码,以有效地将这64位用于更大的数字空间加上NaN和+/-无穷大,因此在精确表示的数字之间存在间隙,只填充近似的数字。
不幸的是,0.3存在差距。
你试过胶带解决方案了吗?
尝试确定错误发生的时间,并用简短的if语句修复它们,这并不漂亮,但对于某些问题,这是唯一的解决方案,这就是其中之一。
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
我在c#的一个科学模拟项目中也遇到过同样的问题,我可以告诉你,如果你忽视蝴蝶效应,它会变成一条大胖龙,咬你一口**