浮点数学是否破碎?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浮点数学是否破碎?相关的知识,希望对你有一定的参考价值。

请考虑以下代码:

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

为什么会出现这些不准确之处?

答案

二进制floating point数学是这样的。在大多数编程语言中,它基于IEEE 754 standardjavascript使用64位浮点表示,这与Java的double相同。问题的关键在于数字以这种格式表示为2的幂的整数倍;有理数(如0.1,即1/10),其分母不是2的幂,无法准确表示。

对于标准0.1格式的binary64,表示可以完全按照

  • 0.1000000000000000055511151231257827021181583404541015625十进制,或
  • 0x1.999999999999ap-4C99 hexfloat notation

相比之下,有理数0.1,即1/10,可以写成完全相同的

  • 0.1十进制,或
  • 0x1.99999999999999...p-4与C99 hexfloat符号类似,其中...代表9的无限序列。

程序中的常数0.20.3也将近似于它们的真实值。碰巧最接近double0.2大于有理数0.2但是最接近的double0.3小于有理数0.30.10.2的总和大于有理数0.3,因此不同意你的代码中的常数。

What Every Computer Scientist Should Know About Floating-Point Arithmetic是一个相当全面的浮点运算问题处理方法。有关更容易理解的解释,请参阅floating-point-gui.de

附注:所有位置(基数为N)的数字系统都精确地共享这个问题

普通的旧十进制(基数为10)数字具有相同的问题,这就是为什么像1/3这样的数字最终为0.333333333 ...

你只是偶然发现了一个很容易用十进制表示的数字(3/10),但不适合二进制系统。它也是两种方式(在某种程度上):1/16是一个十进制的丑陋数字(0.0625),但在二进制中它看起来像十进制中的第10,000个一样整洁(0.0001)** - 如果我们在在我们的日常生活中使用基数2系统的习惯,你甚至可以看到这个数字,并且本能地理解你可以通过减少一些东西,再次将它减半,一次又一次地到达那里。

**当然,这并不是浮点数如何存储在内存中(它们使用科学记数法的形式)。然而,它确实说明了二进制浮点精度误差往往会突然出现,因为我们通常有兴趣使用的“真实世界”数字通常是10的幂 - 但仅仅因为我们使用十进制数字系统日 - 今天。这也是为什么我们会说71%而不是“每7个中有5个”(71%是近似值,因为5/7不能用任何十进制数精确表示)。

所以没有:二进制浮点数没有被破坏,它们碰巧与其他每个base-N数系统一样不完美:)

侧面不:在编程中使用浮点数

实际上,这个精度问题意味着您需要使用舍入函数将浮点数舍入到显示它们之前感兴趣的小数位数。

您还需要使用允许一定容差的比较替换相等测试,这意味着:

不要做if (float1 == float2) { ... }

而是做if (Math.Abs(float1 - float2) < myToleranceValue) { ... }

myToleranceValue可以是1/2 ^ 16(0.0000152587890625)。在Javascript中,提供值Number.EPSILON供您用作容差。

另一答案

我的解决方法:

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

precision是指在添加期间小数点后要保留的位数。

另一答案

已发布了许多好的答案,但我想再追加一个。

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

引擎盖下的商店实数模型表示浮点数

enter image description here

即使你可以轻松输入0.2FLT_RADIXDBL_RADIX也是2;对于具有FPU的计算机,使用“IEEE二进制浮点运算标准(ISO / IEEE Std 754-1985)”不是10。

因此,准确表示这些数字有点困难。即使您明确指定此变量而没有任何中间计算。

另一答案

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

当使用0.1(从0.1到100)的步长添加所有值(a + b)时,我们有大约15%的精度误差。请注意,错误可能会导致稍大或稍小的值。这里有些例子:

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%的精度误差概率。这里有些例子:

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。使用2位十进制数字(步骤0.01),情况会恶化一些(18%和36%)。

另一答案

No, not broken, but most decimal fractions must be approximated

摘要

浮点运算是精确的,不幸的是,它与我们通常的基数为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是不精确的”,但事实并非如此。

我们不断给FP硬件提供基本10中看似简单的东西,但它是基础2中的重复部分。

这怎么发生的?

当我们用十进制写时,每个分数(具体地说,每个终止小数)都是一个有理数的形式

A /(Exximity)

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

A / N.

所以在十进制中,我们不能代表1/3。因为基数10包括2作为素数因子,所以我们可以写为二进制分数的每个数也可以写为基数10分数。但是,我们写的任何基本10分数几乎都不能用二进制表示。在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。

因此,虽然每个二进制分数都可以用十进制写,但反之则不然。事实上,大多数小数部分以二进制重复。

处理它

开发人员通常被指示进行<epsilon比较,更好的建议可能是舍入到整数值(在C库中:round()和roundf(),即保持FP格式)然后进行比较。舍入到特定的小数部分长度解决了输出的大多数问题。

此外,关于真实的数字运算问题(FP在早期,可怕的昂贵计算机上发明的问题),宇宙的物理常数和所有其他测量仅为相对较少的有效数字所知,因此整个问题空间反正是“不精确”。 FP“准确度”在这种应用中不是问题。

当人们尝试使用FP进行bean计数时,整个问题就出现了。它确实适用于此,但只有当你坚持使用积分值时才会这样做,哪种方式会失败。这就是我们拥有所有那些小数部分软件库的原因。

我喜欢Chris的披萨答案,因为它描述了实际的问题,而不仅仅是通常关于“不准确”的问题。如果FP只是“不准确”,我们可以解决这个问题并且几十年前就已经完成了。我们之所以没有这个原因,是因为FP格式紧凑而且速度快,而且它是压缩大量数字的最佳方式。此外,它是太空时代和军备竞赛的遗产,也是使用小型内存系统解决大型问题的早期尝试。 (有时,单个磁芯用于1位存储,但那是another story.

结论

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

另一答案

你尝试过胶带解决方案了吗?

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

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

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

另一答案

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

有大多数小数无法以二进制或十进制或两者精确表示。结果 - 舍入(但精确)数字结果。

另一答案

这个问题的许多重复问题都是关于浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的精确结果而不仅仅是阅读它,更容易了解它的工作原理。有些语言提供了这样做的方法 - 比如在Java中将floatdouble转换为BigDecimal

由于这是一个与语言无关的问题,因此它需要与语言无关的工具,例如Decimal to Floating-Point Converter

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

0.1转换为0.1000000000000000055511151231257827021181583404541015625,

0.2转换为0.200000000000000011102230246251565404236316680908203125,

0.3转换为0.299999999999999988897769753748434595763683319091796875,并且

0.30000000000000004转换为0.3000000000000000444089209850062616169452667236328125。

手动或在十进制计算器(如Full Precision Calculator)中添加前两个数字,显示实际输入的确切总和为0.3000000000000000166533453693773481063544750213623046875。

如果它向下舍入到等于0.3,则舍入误差将为0.0000000000000000277555756156289135105907917022705078125。舍入到相当于0.30000000000000004也会给出舍入误差0.0000000000000000277555756156289135105907917022705078125。适用于圆形甚至平衡断路器。

返回到浮点转换器,0.30000000000000004的原始十六进制是3fd3333333333334,以偶数结束,因此是正确的结果。

另一答案

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

计入三进制,三分之一

以上是关于浮点数学是否破碎?的主要内容,如果未能解决你的问题,请参考以下文章

如何将浮点矩阵作为 2D 纹理传递给片段着色器?

C 语言:使用计算初始化浮点变量

C# 中的浮点数学是不是一致?是真的吗?

浮点数学被破坏了吗?

浮点数学被破坏了吗?

是否可以在 PyQt/PySide2 中为 QLineEdit 的文本制作“破碎”边框