关于语言不可知论:浮点数学是否被打破?

Is floating point math broken?

请考虑以下代码:

1
0.1 + 0.2 == 0.3  ->  false
1
0.1 + 0.2         ->  0.30000000000000004

为什么会出现这些错误?


二进制浮点数学是这样的。在大多数编程语言中,它是基于IEEE754标准的。JavaScript使用64位浮点表示,这与Java的EDCOX1×0相同。问题的关键在于,用这种格式表示的数字是整数乘以二的幂;分母不是二的幂的有理数(如0.1,即1/10)不能精确表示。

对于标准binary64格式的0.1来说,表示可以精确地写为

  • 十进制的0.1000000000000000055511151231257827021181583404541015625,或
  • c99十六进制记数法中的0x1.999999999999ap-4

相反,有理数0.11/10可精确地写为

  • 十进制的0.1,或
  • 0x1.99999999999999...p-4,类似于c99十六进制表示法,其中...表示9的无终止序列。

程序中的常量0.20.3也将近似于它们的真实值。与0.2最接近的double比有理数0.2大,而与0.3最接近的double比有理数0.3小。0.10.2的总和最终大于有理数0.3,因此与代码中的常量不一致。

对浮点运算问题的一个相当全面的处理是每个计算机科学家都应该知道的浮点运算。要获得更易于理解的解释,请参阅floating-point-gui.de。


硬件设计师的视角

我认为我应该添加一个硬件设计师的视角,因为我设计和构建了浮点硬件。了解错误的来源可能有助于理解软件中正在发生的事情,最终,我希望这有助于解释浮点错误发生的原因,并似乎随着时间的推移而累积。好的。1。概述

从工程的角度来看,大多数浮点运算都会有一些错误元素,因为进行浮点运算的硬件只需要最后一个单元的误差小于一半。因此,对于单次操作而言,许多硬件都将以精度停止,只需在最后一个位置产生小于一个单元一半的误差,这在浮点除法中尤其有问题。单个操作的构成取决于单元需要多少个操作数。大多数情况下,它是两个,但有些单位需要3个或更多的操作数。因此,不能保证重复的操作会导致期望的错误,因为错误会随着时间的推移而累积。好的。2。标准

大多数处理器遵循IEEE-754标准,但有些处理器使用非规范化或不同的标准。. 例如,在IEEE-754中有一种非规范化模式,它允许以牺牲精度的代价表示非常小的浮点数。然而,以下内容将涵盖标准化的IEEE-754模式,这是典型的操作模式。好的。

在IEEE-754标准中,只要硬件设计人员在最后一个位置的误差/epsilon小于一个单元的一半,并且一次操作的结果只需在最后一个位置的误差/epsilon小于一个单元的一半。这就解释了为什么当有重复的操作时,错误会累积起来。对于IEEE-754双精度,这是第54位,因为53位用于表示浮点数(例如5.3e 5中的5.3)的数字部分(标准化),也称为尾数。下一节将详细介绍各种浮点运算中硬件错误的原因。好的。三。除法中舍入误差的原因

浮点除法误差的主要原因是用来计算商的除法。大多数计算机系统都是用反乘来计算除法,主要是在Z=X/YZ = 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的最后一个位置使用四舍五入到最接近的偶数这样的方法的原因,因为随着时间的推移,误差更容易相互抵消,并且间隔算法结合了IEEE 754四舍五入模式的变化来预测舍入器。并纠正错误。由于与其他舍入模式相比相对误差较低,舍入到最近的偶数(最后一位)是IEEE-754的默认舍入模式。好的。

注意,默认的四舍五入模式(在最后一个位置四舍五入到最接近的偶数)保证一次操作的最后一个位置的误差小于一个单元的一半。仅使用截断、向上取整和向下取整可能会导致错误,在最后一个位置大于一个单位的一半,但在最后一个位置小于一个单位,因此建议不要使用这些模式,除非它们用于区间算术。好的。6。总结

总之,浮点运算中出现错误的根本原因是硬件中的截断和除法中倒数的截断相结合。由于IEEE-754标准只要求一次操作最后一个单元的误差小于一半,因此重复操作的浮点误差相加,除非纠正。好的。好啊。


当你把.1或1/10转换成基2(二进制)时,你会得到一个小数点后的重复模式,就像试图在基10中表示1/3一样。该值不精确,因此不能使用普通的浮点方法进行精确的数学运算。


这里的大多数答案都用非常枯燥的技术术语来解决这个问题。我想用普通人能理解的术语来解释这个问题。

想象一下你正试图切比萨饼。你有一个机器人切披萨刀,可以把披萨切成两半。它可以将整个比萨饼减半,也可以将现有的一片减半,但无论如何,减半总是准确的。

这个比萨切割机的动作非常精细,如果你从一个完整的比萨开始,然后把它切成两半,继续每次把最小的比萨切成两半,你可以在比萨片太小而不能达到它的高精度之前把它切成两半53次。在这一点上,您不能再将非常薄的切片减半,但必须按原样包括或排除它。

现在,你如何将所有的切片拼成一个比萨饼的十分之一(0.1)或五分之一(0.2)呢?好好想一想,然后试着解决问题。你甚至可以尝试使用真正的比萨,如果你有一个神话般的精密比萨切割机在手。-)

当然,大多数有经验的程序员都知道真正的答案,那就是无论你切得多细,都无法用这些切片精确地将披萨的十分之一或五分之一切成小块。你可以做一个很好的近似,如果你把0.1的近似值加上0.2的近似值,你会得到一个很好的0.3的近似值,但它仍然是一个近似值。

对于双精度数字(允许您将比萨饼减半53倍的精度),立即小于或大于0.1的数字为0.09999999999991673327315311325946822276248931884765625和0.1000000000000005551115123157827021181583404541015625。后者比前者更接近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.1000000000000005551115123157827021181583404541015625+0.200000000000000111022302462515655404236316680908203125=0.300000000000000444089208500626616945267236328125,而最接近0.3的数字实际上是0.299999999999999988897769753748434595763683319091796875。

另外,一些编程语言还提供了可以将切片精确分割成十分之一的比萨切割机。尽管这种切披萨刀并不常见,但如果你有机会切披萨刀,你应该在切到十分之一或五分之一的披萨刀时使用它。

(最初发布在Quora上。)


浮点舍入错误。由于缺少5的素数因子,0.1在基-2中不能像在基-10中那样精确表示。正如1/3取无穷多的数字以十进制表示,但以3为底为"0.1",0.1取无穷多的数字以2为底,而不是以10为底。计算机没有无限的内存。


除了其他正确的答案外,您可能还需要考虑调整值以避免浮点运算出现问题。

例如:

1
var result = 1.0 + 2.0;     // result === 3.0 returns true

…而不是:

1
var result = 0.1 + 0.2;     // result === 0.3 returns false

表达式0.1 + 0.2 === 0.3在javascript中返回false,但幸运的是浮点整数算法是精确的,因此可以通过缩放避免小数表示错误。

作为一个实际的例子,为了避免精度最重要的浮点问题,建议1以表示美分数的整数形式处理货币:2550美分,而不是25.50美元。

1道格拉斯·克罗克福德:javascript:好的部分:附录A-糟糕的部分(第105页)。


我的答案很长,所以我把它分成三部分。既然这个问题是关于浮点数学的,我就把重点放在机器的实际功能上。我还将它指定为双精度(64位),但该参数同样适用于任何浮点运算。好的。

序言好的。

一个IEEE754双精度二进制浮点格式(binary64)数字表示形式的数字好的。

value = (-1)^s * (1.m51m50...m2m1m0)2 * 2e-1023

Ok.

64位:好的。

  • 第一个位是符号位:1,如果数字为负数,则0,否则为1。
  • 接下来的11位是指数,其偏移量为1023。换句话说,从双精度数中读取指数位后,必须减去1023才能得到2的幂。
  • 剩下的52位是有效位(或尾数)。在尾数中,"隐含的"1.总是被省略,因为任何二进制值的最高有效位都是1

1-ieee 754允许有符号零的概念,+0-0被区别对待:1 / (+0)是正无穷大;1 / (-0)是负无穷大。对于零值,尾数和指数位都是零。注:零值(+0和-0)明确不归为非正规2。好的。

2-非正规数不是这样,它的偏移指数为零(和隐含的0.)。非正规双精度数字的范围是dmin≤x≤dmax,其中dmin(最小的可表示非零数字)是2-1023-51(≈4.94*10-324)和dmax(最大的非正规数字,尾数完全由1s组成)是2-1023+1-2-1023-51(≈2.225*10- 308)。好的。

将双精度数转换为二进制数好的。

有许多在线转换器可以将双精度浮点数转换为二进制数(例如binary convert.com),但这里有一些示例C代码可以获得双精度数的IEEE 754表示(我用冒号(:分隔这三个部分):好的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

直截了当:最初的问题好的。

(对于TL,跳到底部;DR版本)好的。

卡托·约翰斯顿(提问者)问为什么是0.1+0.2!= 0.3。好的。

用二进制(用冒号分隔三个部分)编写的IEEE754表示值为:好的。

1
2
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

注意尾数是由0011的重复数字组成。这就是为什么计算中存在任何错误的关键所在——0.1、0.2和0.3不能用二进制精确地表示为有限位数的二进制,任何超过1/9、1/3或1/7的二进制都可以用十进制精确地表示。好的。

还要注意,我们可以将指数的幂减少52,并将二进制表示中的点向右移动52位(很像10-3*1.23==10-5*123)。这样我们就可以将二进制表示法表示为它以a*2p的形式表示的确切值,其中"a"是一个整数。好的。

将指数转换为十进制,删除偏移量,并重新添加隐含的1(在方括号中),0.1和0.2为:好的。

1
2
3
4
5
0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125

要添加两个数字,指数必须相同,即:好的。

1
2
3
4
5
6
7
0.1 => 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum =  2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397  = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794  = 0.200000000000000011102230246251565404236316680908203125
sum =  2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875

由于和的形式不是2n*1。bbb我们将指数增加一,并移动小数(二进制)点以得到:好的。

1
2
sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)
    = 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875

现在尾数中有53位(第53位在上面一行的方括号中)。IEEE754的默认舍入模式是"舍入到最近"——即,如果数字x介于两个值a和b之间,则选择最低有效位为零的值。好的。

1
2
3
4
5
6
7
a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
  = 2^-2  * 1.0011001100110011001100110011001100110011001100110011

x = 2^-2  * 1.0011001100110011001100110011001100110011001100110011(1)

b = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
  = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

注意A和B只在最后一个位上有所不同:...0011+1=...0100。在这种情况下,最低有效位为零的值是b,所以和是:好的。

1
2
sum = 2^-2  * 1.0011001100110011001100110011001100110011001100110100
    = 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125

而0.3的二进制表示是:好的。

1
2
0.3 => 2^-2  * 1.0011001100110011001100110011001100110011001100110011
    =  2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875

它只与0.1和0.2之和的二进制表示法的2-54不同。好的。

0.1和0.2的二进制表示是IEEE 754所允许的数字的最精确表示。由于默认的舍入模式,添加这些表示形式会产生一个只在最低有效位上有所不同的值。好的。

DR好的。

在IEEE754二进制表示中写入0.1 + 0.2(用冒号分隔三部分)并将其与0.3进行比较,这是(我将不同的位放在方括号中):好的。

1
2
0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

转换回十进制,这些值为:好的。

1
2
0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

与原始值相比,差异正好为2-54,约为5.5511151231258×10-17-无意义(对于许多应用)。好的。

比较浮点数的最后几位本身就很危险,因为任何读过著名的"每个计算机科学家都应该知道的关于浮点数算术的知识"(涵盖了这个答案的所有主要部分)的人都会知道。好的。

大多数计算器使用额外的保护数字来解决这个问题,这就是0.1 + 0.2将给0.3的方式:最后几个位是四舍五入的。好的。好啊。


存储在计算机中的浮点数由两部分组成,一个整数和一个指数,基数被取为整数部分并乘以整数部分。

如果计算机以10为基数工作,那么0.1就是1 x 10?10.2就是2 x 10?10.3就是3 x 10?1。整数数学既简单又精确,所以加上0.1 + 0.2显然会得到0.3

计算机通常不在10进制下工作,而是在2进制下工作。对于某些值,您仍然可以得到精确的结果,例如,0.51 x 2?10.251 x 2?2,将其添加到3 x 2?20.75。确切地。

问题在于数字可以以10为基数精确表示,但不能以2为基数。这些数字需要四舍五入到最接近的等价物。假设非常常见的IEEE 64位浮点格式,最接近0.1的数字是3602879701896397 x 2???,最接近0.2的数字是7205759403792794 x 2???;将它们加在一起会得到10808639105689191 x 2???0.3000000000000000444089209850062616169452667236328125的精确十进制值。浮点数通常四舍五入显示。


浮点舍入错误。从每个计算机科学家对浮点运算的了解来看:

Squeezing infinitely many real numbers into a finite number of bits requires an approximate representation. Although there are infinitely many integers, in most programs the result of integer computations can be stored in 32 bits. In contrast, given any fixed number of bits, most calculations with real numbers will produce quantities that cannot be exactly represented using that many bits. Therefore the result of a floating-point calculation must often be rounded in order to fit back into its finite representation. This rounding error is the characteristic feature of floating-point computation.


我的工作方法:

1
2
3
4
function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

精度是指在加法过程中要保留在小数点后的位数。


很多好的答案都贴出来了,但我想再附加一个。

并非所有数字都可以通过浮点/双精度表示例如,在IEEE754浮点标准中,数字"0.2"将以单精度表示为"0.200000003"。

引擎盖下存储实数的模型将浮点数表示为

enter image description here

即使您可以轻松键入0.2FLT_RADIXDBL_RADIX是2;对于使用"IEEE二进制浮点运算标准(ISO/IEEE Std 754-1985)"的FPU的计算机,则不是10。

所以准确地表示这些数字有点困难。即使在没有任何中间计算的情况下显式指定此变量。


一些统计数据与这个著名的双精度问题有关。

当使用0.1(从0.1到100)的阶跃添加所有值(A+B)时,我们有大约15%的精度误差几率。请注意,该错误可能导致值稍大或稍小。以下是一些例子:

1
2
3
4
5
6
7
8
0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)

当用0.1(从100到0.1)的阶跃减去所有值(a-b,其中a>b)时,我们有大约34%的精度误差几率。以下是一些例子:

1
2
3
4
5
6
7
8
0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)

*15%和34%确实很大,所以在精度很重要的时候总是使用bigdecimal。小数点后两位(步骤0.01)情况会进一步恶化(18%和36%)。


不,不是断开的,但大多数小数必须近似

Summary

浮点运算是精确的,不幸的是,它与我们通常使用的基数10表示法不太匹配,所以我们经常给它输入与我们所写的稍有不同的输入。

即使是0.01,0.02,0.03,0.04这样的简单数字…0.24不能精确地表示为二进制分数。如果你数到0.01,.02,.03…,直到你数到0.25,你会得到第一个分数,在base2中表示。如果你尝试使用fp,你的0.01会稍微偏离,所以唯一的方法是把25个加起来精确到0.25,这就需要一长串的因果关系,包括保护位和舍入。很难预测,所以我们举手说"外交政策不准确",但事实并非如此。

我们经常给fp硬件一些在基10中看起来很简单,但在基2中是一个重复分数。

How did this happen?

当我们用十进制书写时,每个分数(特别是每个终止的十进制)都是形式的有理数。

&(a)A/(2n×5m)

在二进制中,我们只得到2n项,即:

&A/2N

所以在十进制中,我们不能表示1/3。因为基10包含2作为素数因子,所以我们可以写为二进制分数的每个数字也可以写为基10分数。然而,几乎没有任何东西我们写为一个基础10fraction是代表二进制。在0.01、0.02、0.03…0.99,只有三个数字可以用我们的fp格式表示:0.25、0.50和0.75,因为它们是1/4、1/2和3/4,所有数字都有一个仅使用2n项的素数因子。

在Base10中,我们不能代表1/3。但是在二进制文件中,我们不能做1/10或1/3。

所以,虽然每个二进制分数都可以用十进制写,但反过来就不是真的了。事实上,大多数十进制分数都是二进制的。

Dealing with it

开发人员通常被要求进行

此外,对于实际的数字处理问题(早期非常昂贵的计算机上发明了FP的问题),宇宙的物理常数和所有其他测量值只知道相对较少的有效数字,因此整个问题空间无论如何都是"不精确的"。在这种应用中,"精确性"不是问题。

当人们试图使用fp进行bean计数时,整个问题就真的出现了。它确实能做到这一点,但前提是你坚持积分,哪种方法会破坏使用积分的意义。这就是为什么我们有所有这些小数部分软件库的原因。

我喜欢克里斯的披萨回答,因为它描述的是实际问题,而不仅仅是惯常的关于"不准确"的手势。如果FP只是"不准确",我们可以解决这个问题,并且在几十年前就已经做到了。我们没有这样做的原因是因为fp格式紧凑快速,是处理大量数字的最佳方法。同时,它也是太空时代和军备竞赛的遗产,也是早期使用小型内存系统的低速计算机解决大问题的尝试。(有时,单个磁芯用于1位存储,但这是另一回事。)

Conclusion

如果您只是在一个银行计算bean,那么首先使用十进制字符串表示的软件解决方案就可以很好地工作。但你不能这样做量子色动力学或空气动力学。


你试过管道胶带溶液吗?

尝试确定错误发生的时间,并用简短的if语句修复错误,这不是很好,但对于某些问题,它是唯一的解决方案,这是其中之一。

1
2
 if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
                    else { return n * 0.1 + 0.000000000000001 ;}

我在C的一个科学模拟项目中遇到了同样的问题,我可以告诉你,如果你忽视蝴蝶效应,它会变成一条大肥龙,在A中咬你**


这些奇怪的数字出现是因为计算机使用二进制(以2为基数)数字系统进行计算,而我们使用十进制(以10为基数)。

大多数分数都不能用二进制或十进制或两者都精确表示。结果-一个四舍五入(但精确)的数字结果。


这个问题的许多重复问题都询问了浮点取整对特定数字的影响。在实践中,通过查看利息计算的准确结果而不是仅仅阅读利息计算结果,更容易了解利息是如何工作的。一些语言提供了这样做的方式,例如在Java中将EDCOX1 0或EDCOX1 1的转换为EDCOX1×2。

由于这是一个语言不可知论问题,因此它需要语言不可知论工具,例如十进制到浮点转换器。

将其应用于问题中的数字,视为双精度:

0.1转换为0.10000000000000055511151231257827021181583404541015625,

0.2转换为0.2000000000000001110223062515655404236316680908203125,

0.3转换为0.29999999999988897769753748434595763683319091796875,以及

0.300000000004转换为0.300000000000000444089085006261616945267236328125。

手动添加前两个数字或在全精度计算器等十进制计算器中,显示实际输入的精确和为0.30000000000000016653345369377348106354750213623046875。

如果四舍五入到0.3,则四舍五入误差为0.0000000000277555756156289135105907917022705078125。四舍五入到相当于0.300000000004的数值,也会产生四舍五入误差0.0000000000277555756156289135105907917022705078125。圆到均匀的平局适用。

返回浮点转换器时,0.300000000004的原始十六进制是3FD3333333333333334,以偶数结尾,因此是正确的结果。


我可以补充一句,人们总是认为这是一个计算机问题,但如果你用手数数(以10为基数),你就不能得到(1/3+1/3=2/3)=true,除非你有无穷大可以加0.333…到0.333…因此,就像基2中的(1/10+2/10)!==3/10问题一样,将其截断为0.333+0.333=0.666,并将其四舍五入为0.667,这在技术上也是不准确的。

以三元计算,但三分之一不是问题-也许有些比赛每只手上有15个手指会问为什么你的十进制数学被打破…


既然没人提到这件事…

一些高级语言(如Python和Java)具有克服二进制浮点限制的工具。例如:

  • Python的EDCOX1×0模块和Java的EDCOX1×1类,用十进制表示内部的数字(而不是二进制记号)。两者的精度都很有限,所以仍然容易出错,但是它们用二进制浮点算法解决了最常见的问题。

    小数在处理钱时非常好:十美分加二十美分总是正好三十美分:

    1
    2
    3
    4
    >>> 0.1 + 0.2 == 0.3
    False
    >>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
    True

    python的decimal模块基于IEEE标准854-1987。

  • python的fractions模块和apache common的BigFraction类。两种方法都将有理数表示为(numerator, denominator)对,与十进制浮点算法相比,它们可以给出更精确的结果。

这两种解决方案都不是完美的(特别是如果我们考虑性能,或者我们需要非常高的精度),但是它们仍然用二进制浮点算法解决了大量的问题。


为了提供最好的解决方案,我可以说我发现了以下方法:

1
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3

我来解释一下为什么这是最好的解决方案。正如上面提到的其他人所回答的,最好使用随时可用的javascript tofixed()函数来解决这个问题。但很可能你会遇到一些问题。

假设你要加上两个浮点数,如0.20.7,这里是:0.2 + 0.7 = 0.8999999999999999

您期望的结果是0.9,这意味着在这种情况下需要1位精度的结果。所以你应该使用(0.2 + 0.7).tofixed(1)。但是不能只给tofixed()一个特定的参数,因为它取决于给定的数字,例如

1
`0.22 + 0.7 = 0.9199999999999999`

在这个例子中,您需要2位精度,所以它应该是toFixed(2),那么参数应该是什么来适应每个给定的浮点数?

你可能会说在每种情况下都是10:

1
(0.2 + 0.7).toFixed(10) => Result will be"0.9000000000"

该死!你打算怎么处理那些9点以后不需要的零?现在是时候将其转换为float来实现您所希望的:

1
parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9

既然您找到了解决方案,那么最好将其作为如下函数提供:

1
2
3
function floatify(number){
           return parseFloat((number).toFixed(10));
        }

让我们自己试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
function floatify(number){
       return parseFloat((number).toFixed(10));
    }
 
function addUp(){
  var number1 = +$("#number1").val();
  var number2 = +$("#number2").val();
  var unexpectedResult = number1 + number2;
  var expectedResult = floatify(number1 + number2);
  $("#unexpectedResult").text(unexpectedResult);
  $("#expectedResult").text(expectedResult);
}
addUp();
1
2
3
4
5
6
7
8
9
input{
  width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
1
2
3
4
5
6
7
8
9
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>
Expected Result: <span id="expectedResult"></span>
</p>
<p>
Unexpected Result: <span id="unexpectedResult"></span>
</p>

您可以这样使用它:

1
2
var x = 0.2 + 0.7;
floatify(x);  => Result: 0.9

正如W3Schools所建议的,还有另一个解决方案,您可以通过乘法和除法来解决上述问题:

1
var x = (0.2 * 10 + 0.1 * 10) / 10;       // x will be 0.3

记住,虽然看起来是一样的,但(0.2 + 0.1) * 10 / 10根本不起作用!我更喜欢第一个解决方案,因为我可以将它作为一个函数应用,将输入浮点转换为精确的输出浮点。


可以在数字计算机中实现的浮点数学必须使用实数和运算的近似值。(标准版有超过50页的文档,并有一个委员会来处理它的勘误表和进一步的改进。)

这种近似是不同种类的近似的混合,每一种近似都可以被忽略,或者由于其偏离正确性的特殊方式而被仔细考虑。它还涉及硬件和软件级别的一些明显的异常情况,大多数人都会假装没有注意到这些情况而直接走过。

如果你需要无限的精度(例如使用数字π,而不是它的许多较短的站立点中的一个),你应该编写或使用一个符号数学程序来代替。

但是,如果您同意这样一个观点,即有时浮点数学在值和逻辑上是模糊的,并且错误可以快速累积,并且您可以编写需求和测试来考虑这一点,那么您的代码可以经常使用您的FPU中的内容。


为了好玩,我使用了浮点数的表示,遵循标准C99的定义,并编写了下面的代码。

代码以3个独立的组打印浮点数的二进制表示。

1
SIGN EXPONENT FRACTION

然后它输出一个和,当有足够的精度求和时,它将显示硬件中真正存在的值。

因此,当编写float x = 999...时,编译器将以由函数xx打印的位表示形式转换该数字,使函数yy打印的和等于给定的数字。

实际上,这个和只是一个近似值。对于数字999999999,编译器将在浮点的位表示形式中插入数字100000000。

在代码之后,我附加了一个控制台会话,在该会话中,我计算由编译器插入的硬件中实际存在的两个常量(减去pi和99999999)的项和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#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("
");
}

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("
");
}

void
main()
{
    float x=-3.14;
    float y=999999999;
    printf("%lu
", sizeof(x));
    xx(&x);
    xx(&y);
    yy(x);
    yy(y);
}

这里是一个控制台会话,我在其中计算硬件中存在的浮点值的实际值。我用bc打印主程序输出的术语和。可以在python repl或类似的代码中插入这个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- .../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是

1
999999999.999999446351872

你也可以和bc确认-3.14也受到干扰。别忘了在bc中设置一个scale因子。

显示的总和是硬件中的内容。通过计算得到的值取决于设置的比例。我确实把scale系数设为15。从数学上讲,它的精度是无限的,似乎是100000000。


另一种方法是:使用64位来表示数字。因此,无法精确表示超过2**64=18446744073709551616的不同数字。

然而,数学表示0到1之间已经有无限多的小数。IEE754定义了一种编码,以便在更大的数字空间加上NaN和+/-Infinity中有效地使用这64位,因此精确表示的数字之间存在间隙,这些数字只填充了近似的数字。

不幸的是,0.3处在一个缺口中。


由于这个线程对当前的浮点实现进行了一些一般性的讨论,我想补充一点,有一些项目正在解决它们的问题。

例如,看看https://posthub.org/,它展示了一个名为posit(及其前身unum)的数字类型,它承诺以更少的位提供更好的准确性。如果我的理解是正确的,它也解决了问题中的问题。相当有趣的项目,它背后的人是一个数学家IT博士约翰·古斯塔夫森。整个事情都是开源的,在C/C++、Python、朱丽亚和C*~(http://Histelay.com/算术)中有很多实际的实现。


由于python 3.5,您可以使用math.isclose()函数来测试近似相等:

1
2
3
4
5
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False


假设以10为基数,精确到8位。你检查一下

1
1/3 + 2 / 3 == 1

并了解这将返回false。为什么?好吧,我们有实数

1/3=0.333….2/3=0.666….

在小数点后8位截断,我们得到

1
0.33333333 + 0.66666666 = 0.99999999

当然,与1.00000000完全不同的是0.00000001

具有固定位数的二进制数的情况完全类似。作为实数,我们有

1/10=0.000110011001100…(基地2)

1/5=0.0011001100110011001…(基地2)

如果我们把这些截断为,比如说,七位,那么我们将得到

1
0.0001100 + 0.0011001 = 0.0100101

另一方面,

3/10=0.0100110011…(基地2)

它被截短为7位,是0.0100110,它们与0.0000001完全不同。

由于这些数字通常以科学记数法存储,所以确切的情况稍微微妙一些。因此,例如,我们不将1/10存储为0.0001100,而是将其存储为类似1.10011 * 2^-4,这取决于我们为指数和尾数分配了多少位。这会影响计算精度的位数。

结果是,由于这些舍入错误,您根本不想在浮点数字上使用==。相反,您可以检查它们差异的绝对值是否小于某个固定的小数字。


math.sum(javascript)….操作员替换类型

1
2
.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
Object.defineProperties(Math, {
    sign: {
        value: function (x) {
            return x ? x < 0 ? -1 : 1 : 0;
            }
        },
    precision: {
        value: function (value, precision, type) {
            var v = parseFloat(value),
                p = Math.max(precision, 0) || 0,
                t = type || 'round';
            return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
        }
    },
    scientific_to_num: {  // this is from https://gist.github.com/jiggzson
        value: function (num) {
            //if the number is in scientific notation remove it
            if (/e/i.test(num)) {
                var zero = '0',
                        parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
                        e = parts.pop(), //store the exponential part
                        l = Math.abs(e), //get the number of zeros
                        sign = e / l,
                        coeff_array = parts[0].split('.');
                if (sign === -1) {
                    num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
                } else {
                    var dec = coeff_array[1];
                    if (dec)
                        l = l - dec.length;
                    num = coeff_array.join('') + new Array(l + 1).join(zero);
                }
            }
            return num;
         }
     }
    get_precision: {
        value: function (number) {
            var arr = Math.scientific_to_num((number +"")).split(".");
            return arr[1] ? arr[1].length : 0;
        }
    },
    diff:{
        value: function(A,B){
            var prec = this.max(this.get_precision(A),this.get_precision(B));
            return +this.precision(A-B,prec);
        }
    },
    sum: {
        value: function () {
            var prec = 0, sum = 0;
            for (var i = 0; i < arguments.length; i++) {
                prec = this.max(prec, this.get_precision(arguments[i]));
                sum += +arguments[i]; // force float to convert strings to number
            }
            return Math.precision(sum, prec);
        }
    }
});

其思想是使用数学而不是运算符来避免浮点错误。

1
2
Math.diff(0.2, 0.11) == 0.09 // true
0.2 - 0.11 == 0.09 // false

还要注意math.diff和math.sum会自动检测要使用的精度

math.sum接受任意数量的参数


另一个问题被命名为此问题的副本:

在C++中,为什么EDCOX1 OR 0的结果与调试器显示EDCOX1×1的值不同?

问题中的xfloat变量。

一个例子是

1
float x = 9.9F;

调试器显示9.89999962cout操作的输出为9.9

答案是,float的默认精度是6,所以它会四舍五入到6位小数。

见此处参考


这实际上是为了回答这个问题——在我整理这个答案的时候,这个问题被作为这个问题的一个副本关闭了,所以现在我不能把它张贴在那里……所以我会在这里发帖!

Question summary:

On the worksheet 10^-8/1000 and 10^-11 evaluate as Equal while in VBA they do not.

在工作表上,数字默认为科学记数法。

如果用15小数点将单元格更改为Number的数字格式(ctrl+1),则得到:

1
2
=10^-11 returns 0.000000000010000
=10^(-8/1000) returns 0.981747943019984

因此,它们肯定是不一样的…一个大约是零,另一个大约是1。

Excel并不是为处理非常小的数字而设计的——至少不是针对库存安装。有一些插件可以帮助提高数字精度。

Excel was designed in accordance to the IEEE Standard for Binary Floating-Point Arithmetic (IEEE 754). The standard defines how floating-point numbers are stored and calculated. The IEEE 754 standard is widely used because it allows-floating point numbers to be stored in a reasonable amount of space and calculations can occur relatively quickly.

The advantage of floating over fixed point representation is that it can support a wider range of values. For example, a fixed-point representation that has 5 decimal digits with the decimal point positioned after the third digit can represent the numbers 123.34, 12.23, 2.45, etc. whereas floating-point representation with 5 digit precision can represent 1.2345, 12345, 0.00012345, etc. Similarly, floating-point representation also allows calculations over a wide range of magnitudes while maintaining precision. For example,

img

其他参考文献:

  • Office支持:以科学(指数)符号显示数字
  • 微软365博客:了解浮点精度,又名"为什么Excel给了我看似错误的答案?""
  • 办公支持:在Excel中设置舍入精度
  • 办公支持:POWER功能
  • 超级用户:我可以在Excel VBA变量中存储的最大值(数字)是多少?