浮点数学被破坏了吗?

Posted

技术标签:

【中文标题】浮点数学被破坏了吗?【英文标题】:Is floating point math broken? 【发布时间】:2022-01-22 10:51:42 【问题描述】:

考虑以下代码:

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

为什么会出现这些错误?

【问题讨论】:

浮点变量通常具有这种行为。这是由它们在硬件中的存储方式引起的。欲了解更多信息,请查看Wikipedia article on floating point numbers。 javascript 将小数视为floating point numbers,这意味着加法等操作可能会出现舍入错误。你可能想看看这篇文章:What Every Computer Scientist Should Know About Floating-Point Arithmetic 仅供参考,javascript 中的所有数字类型都是 IEEE-754 双精度数。 因为 JavaScript 使用 IEEE 754 数学标准,它使用 64 位 浮点数。这会在进行浮点(十进制)计算时导致精度错误,简而言之,由于计算机在 Base 2 中工作,而十进制是 Base 10 简单解释:1/10 是周期性的二进制(0.0 0011 0011 0011...)就像 1/3 是周期性的十进制(0.333...),所以 1/10 不能用浮点数准确表示。 【参考方案1】:

浮点数的陷阱是它们看起来像十进制,但它们以二进制工作。

2 的唯一质因数是 2,而 10 的质因数是 2 和 5。这样的结果是,每个可以精确写成二进制分数的数字也可以精确写成十进制分数,但只有可以写成十进制分数的数字子集可以写成二进制分数。

浮点数本质上是具有有限有效数字的二进制小数。如果超过这些有效数字,则结果将被四舍五入。

当您在代码中键入文字或调用函数将浮点数解析为字符串时,它需要一个十进制数,并将该十进制数的二进制近似值存储在变量中。

当您打印一个浮点数或调用函数将一个转换为字符串时,它会打印浮点数的十进制近似值。 可以将二进制数精确地转换为十进制数,但在转换为字符串* 时,我所知道的任何语言默认情况下都不会这样做。一些语言使用固定数量的有效数字,其他语言使用最短的字符串“往返”返回相同的浮点值。

* Python 确实在将浮点数转换为“decimal.Decimal”时进行精确转换。这是我所知道的获得浮点数的精确十进制等值的最简单方法。

【讨论】:

【参考方案2】:

Since Python 3.5 你可以使用math.isclose() 函数来测试近似相等性:

>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False

【讨论】:

【参考方案3】:

它被破坏的方式与你在小学学习的十进制(base-10)符号被破坏的方式完全相同,只是针对 base-2。

要理解,请考虑将 1/3 表示为十进制值。不可能完全做到!同样,1/10(十进制 0.1)不能以 2 进制(二进制)精确表示为“十进制”值;小数点后的重复模式永远持续下去。该值不准确,因此您无法使用普通浮点方法对其进行精确数学运算。

【讨论】:

伟大而简短的答案。重复模式看起来像 0.00011001100110011001100110011001100110011001100110011... 有些方法可以产生精确的十进制值。 BCD(二进制编码十进制)或各种其他形式的十进制数。然而,这些都比使用二进制浮点更慢(慢很多)并且占用更多的存储空间。 (例如,压缩 BCD 在一个字节中存储 2 个十进制数字。这是一个字节中的 100 个可能值,实际上可以存储 256 个可能值,即 100/256,这浪费了大约 60% 的字节可能值。) @IInspectable,对于浮点运算,基于 BCD 的数学运算比原生二进制浮点慢数百倍。 @DuncanC 嗯,有一些方法可以产生精确的十进制值——用于加法和减法。对于除法、乘法等,它们与二进制方法具有相同的问题。这就是为什么在会计中使用 BCD 的原因,因为它主要处理加号和减号,你不能解释任何小于一美分的东西。然而,像1/3*3 == 1 这样简单的东西在 BCD 数学中失败(评估为假),就像在纸上使用十进制除法一样。 @DuncanC:“BCD 比二进制浮点慢很多,句号。” - 嗯,是的。除非不是。很确定有architectures,其中 BCD 数学至少与 IEEE-754 浮点数学一样快(或更快)。但这不是重点:如果您需要小数精度,则不能使用 IEEE-754 浮点表示。这样做只会实现一件事:更快地计算错误的结果。【参考方案4】:

In short这是因为:

浮点数不能用二进制精确表示所有小数

就像 10/3 一样,does not exist 精确地以 10 为底(它将是 3.33... 重复出现),就像二进制中不存在 1/10。

那又怎样?如何处理?有什么解决办法吗?

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

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() 一个特定的参数,因为它取决于给定的数字,例如

0.22 + 0.7 = 0.9199999999999999

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

那么你可能会说在任何情况下都设为 10:

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

该死!你打算如何处理 9 点之后的那些不需要的零? 是时候将其转换为 float 以使其符合您的要求了:

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

既然您找到了解决方案,最好将它作为这样的函数提供:

function floatify(number)
           return parseFloat((number).toFixed(10));
        
    

让我们自己尝试一下:

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();
input
  width: 50px;

#expectedResult
color: green;

#unexpectedResult
color: red;
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<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>

你可以这样使用它:

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

正如W3SCHOOLS 所暗示的,还有另一种解决方案,您可以乘除以解决上述问题:

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

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

【讨论】:

这让我很头疼。我将 12 个浮点数相加,然后显示这些数字的总和和平均值。使用 toFixed() 可能会修复 2 个数字的总和,但是当对多个数字求和时,飞跃很重要。 @Nuryagdy Mustapayev 我没有明白你的意图,因为我在你可以对 12 个浮点数求和之前进行了测试,然后在结果上使用 floatify() 函数,然后做任何你想做的事情,我使用它没有发现任何问题。 我只是说在我的情况下,我有大约 20 个参数和 20 个公式,每个公式的结果取决于其他公式,这个解决方案没有帮助。【参考方案5】:

二进制floating point数学是这样的。在大多数编程语言中,它基于IEEE 754 standard。问题的症结在于,数字以这种格式表示为整数乘以 2 的幂。分母不是2的幂的有理数(如0.1,即1/10)无法精确表示。

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

0.1000000000000000055511151231257827021181583404541015625 十进制,或 0x1.999999999999ap-4 在C99 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),但在二进制中它看起来就像十进制中的万分之一一样整洁(0.0001)** - 如果我们在在我们的日常生活中使用以 2 为底的数字系统的习惯,你甚至会看到这个数字并本能地理解你可以通过减半来达到那里,一次又一次地减半。

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

所以不:二进制浮点数并没有被破坏,它们只是碰巧和其他所有基于 N 的数字系统一样不完美:)

旁注:在编程中使用浮点数

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

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

不要不要不要if (x == y) ...

改为if (abs(x - y) &lt; myToleranceValue) ...

其中abs 是绝对值。 myToleranceValue 需要为您的特定应用程序选择 - 这与您准备允许多少“摆动空间”以及您要比较的最大数字可能是多少(由于损失)有很大关系精度问题)。请注意您选择的语言中的“epsilon”样式常量。这些可用作公差值。

【讨论】:

我认为“一些错误常数”比“The Epsilon”更正确,因为没有可以在所有情况下使用的“The Epsilon”。在不同的情况下需要使用不同的 epsilon。而且机器 epsilon 几乎从来都不是一个好用的常数。 并非所有浮点数学都基于 IEEE [754] 标准。例如,仍然有一些系统使用旧的 IBM 十六进制 FP,并且仍然有不支持 IEEE-754 算法的显卡。然而,这是一个合理的近似值。 Cray 为了速度而放弃了 IEEE-754 合规性。 Java 也放松了对优化的坚持。 我认为您应该在这个答案中添加一些关于货币计算应该始终、始终使用 整数 上的定点算术的内容,因为货币是量化的。 (以美分的一小部分或任何最小的货币单位进行内部会计计算可能是有意义的——这通常有助于减少将“每月 29.99 美元”转换为每日汇率时的舍入误差——但它应该仍然是定点算术。) 有趣的事实:这个 0.1 在二进制浮点中没有精确表示导致臭名昭著的Patriot missile software bug 在第一次伊拉克战争中导致 28 人丧生。【参考方案6】:

普通算术是以 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) ...

【讨论】:

【参考方案7】:

在硬件级别,浮点数表示为二进制数的分数(以 2 为底)。比如小数部分:

0.125

具有值 1/10 + 2/100 + 5/1000 并且以同样的方式具有二进制分数:

0.001

的值为 0/2 + 0/4 + 1/8。这两个分数的值相同,唯一的区别是第一个是十进制分数,第二个是二进制分数。

不幸的是,大多数十进制分数不能在二进制分数中精确表示。因此,一般情况下,您给出的浮点数仅近似为二进制分数以存储在机器中。

以 10 为底的问题更容易解决。以分数 1/3 为例。您可以将其近似为小数:

0.3

或更好,

0.33

或更好,

0.333

等等。不管你写了多少个小数位,结果永远不会正好是 1/3,但它是一个总是更接近的估计值。

同样,无论您使用多少个以 2 为底的小数位,十进制值 0.1 都不能完全表示为二进制小数。在底数 2 中,1/10 是以下周期数:

0.0001100110011001100110011001100110011001100110011 ...

停在任何有限数量的位上,你会得到一个近似值。

对于 Python,在典型的机器上,浮点数的精度使用 53 位,因此输入十进制 0.1 时存储的值是二进制小数。

0.00011001100110011001100110011001100110011001100110011010

接近但不完全等于 1/10。

由于浮点数在解释器中的显示方式,很容易忘记存储的值是原始小数的近似值。 Python 仅显示以二进制形式存储的值的十进制近似值。如果 Python 要输出存储为 0.1 的二进制近似值的真正十进制值,它将输出:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

这比大多数人预期的要多得多,因此 Python 显示一个四舍五入的值以提高可读性:

>>> 0.1
0.1

重要的是要理解,这实际上是一种错觉:存储的值并不完全是 1/10,只是在显示屏上存储的值被四舍五入。只要您使用这些值执行算术运算,这一点就会变得很明显:

>>> 0.1 + 0.2
0.30000000000000004

这种行为是机器浮点表示的本质所固有的:它不是 Python 中的错误,也不是代码中的错误。您可以在使用硬件支持计算浮点数的所有其他语言中观察到相同类型的行为(尽管某些语言默认情况下不会使差异可见,或者并非在所有显示模式下都可见)。

另一个惊喜是这个固有的。例如,如果您尝试将值 2.675 舍入到小数点后两位,您将得到

>>> round (2.675, 2)
2.67

round() 原语的文档表明它舍入到离零最近的值。由于小数部分正好在 2.67 和 2.68 之间,因此您应该期望得到(二进制近似值)2.68。然而,情况并非如此,因为当小数部分 2.675 转换为浮点数时,它是通过一个近似值存储的,其精确值为:

2.67499999999999982236431605997495353221893310546875

由于近似值比 2.68 更接近 2.67,因此舍入向下。

如果您遇到将十进制数字舍入一半很重要的情况,您应该使用十进制模块。顺便说一句,decimal 模块还提供了一种方便的方法来“查看”为任何浮点数存储的确切值。

>>> from decimal import Decimal
>>> Decimal (2.675)
>>> Decimal ('2.67499999999999982236431605997495353221893310546875')

0.1 不完全存储在 1/10 中的另一个后果是 0.1 的十个值的总和也不等于 1.0:

>>> sum = 0.0
>>> for i in range (10):
... sum + = 0.1
...>>> sum
0.9999999999999999

二进制浮点数的算术具有许多这样的惊喜。 “0.1”的问题在下面的“表示错误”部分中详细解释。有关此类意外的更完整列表,请参阅浮点的危险。

确实没有简单的答案,但是不要过分怀疑浮动虚拟数!在 Python 中,浮点数运算中的错误是由底层硬件引起的,并且在大多数机器上,每次操作的错误率不超过 2 ** 53。这对于大多数任务来说是非常必要的,但您应该记住,这些不是十进制运算,并且对浮点数的每个运算都可能会遇到新的错误。

虽然存在病态案例,但对于大多数常见用例,您只需将显示屏上的小数位数四舍五入即可获得预期结果。有关如何精细控制浮点数的显示,请参阅字符串格式语法了解 str.format () 方法的格式规范。

这部分答案详细解释了“0.1”的例子,并展示了如何自己对这类案例进行精确分析。我们假设您熟悉浮点数的二进制表示。术语表示错误意味着大多数十进制分数不能精确地用二进制表示。这就是为什么 Python(或 Perl、C、C++、Java、Fortran 等)通常不以十进制显示确切结果的主要原因:

>>> 0.1 + 0.2
0.30000000000000004

为什么? 1/10 和 2/10 不能用二进制分数精确表示。但是,今天(2010 年 7 月)的所有机器都遵循 IEEE-754 浮点数算术标准。大多数平台使用“IEEE-754 双精度”来表示 Python 浮点数。双精度 IEEE-754 使用 53 位精度,因此在读取计算机时会尝试将 0.1 转换为 J / 2 ** N 形式的最接近的小数,其中 J 是正好为 53 位的整数。重写:

1/10 ~ = J / (2 ** N)

在:

J ~ = 2 ** N / 10

记住 J 正好是 53 位(所以> = 2 ** 52 但

>>> 2 ** 52
4503599627370496
>>> 2 ** 53
9007199254740992
>>> 2 ** 56/10
7205759403792793

所以 56 是 N 的唯一可能值,它正好为 J 留下 53 位。因此,J 的最佳可能值是这个商,四舍五入:

>>> q, r = divmod (2 ** 56, 10)
>>> r
6

由于进位大于 10 的一半,通过四舍五入获得最佳近似值:

>>> q + 1
7205759403792794

因此,“IEEE-754 双精度”中 1/10 的最佳近似值是 2 ** 56 以上,即:

7205759403792794/72057594037927936

请注意,由于向上舍入,结果实际上略大于 1/10;如果我们没有四舍五入,商将略小于 1/10。但绝不是正好是 1/10!

所以计算机永远不会“看到”1/10:它看到的是上面给出的精确分数,使用“IEEE-754”中的双精度浮点数的最佳近似值:

>>>. 1 * 2 ** 56
7205759403792794.0

如果我们将这个分数乘以 10 ** 30,我们可以观察到它的小数点后 30 位的强权值。

>>> 7205759403792794 * 10 ** 30 // 2 ** 56
100000000000000005551115123125L

意味着存储在计算机中的确切值大约等于十进制值 0.100000000000000005551115123125。在 Python 2.7 和 Python 3.1 之前的版本中,Python 将这些值四舍五入到小数点后 17 位,显示“0.10000000000000001”。在当前版本的 Python 中,显示的值是分数尽可能短的值,而在转换回二进制时给出完全相同的表示,只是显示“0.1”。

【讨论】:

【参考方案8】:

诸如0.10.20.3 之类的十进制数字在二进制编码的浮点类型中不精确表示。 0.10.2 的近似值之和不同于 0.3 的近似值,因此 0.1 + 0.2 == 0.3 的错误可以在此处更清楚地看到:

#include <stdio.h>

int main() 
    printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
    printf("0.1 is %.23f\n", 0.1);
    printf("0.2 is %.23f\n", 0.2);
    printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
    printf("0.3 is %.23f\n", 0.3);
    printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
    return 0;

输出:

0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17

为了更可靠地评估这些计算,您需要对浮点值使用基于十进制的表示。默认情况下,C 标准没有指定此类类型,而是作为 technical Report 中描述的扩展名。

_Decimal32_Decimal64_Decimal128 类型可能在您的系统上可用(例如,GCC 在 selected targets 上支持它们,但 Clang 在 OS X 上不支持它们)。

【讨论】:

【参考方案9】:

我刚刚看到一个关于浮点的有趣问题:

考虑以下结果:

error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1

2**53+1 时我们可以清楚地看到一个断点 - 在2**53 之前一切正常。

>>> (2**53) - int(float(2**53))
0

这是因为双精度二进制:IEEE 754 双精度二进制浮点格式:binary64

来自Double-precision floating-point format 的***页面:

双精度二进制浮点是 PC 上常用的格式,因为它比单精度浮点范围更广,尽管它的性能和带宽成本。与单精度浮点格式一样,与相同大小的整数格式相比,它在整数上缺乏精度。它通常被简单地称为双重。 IEEE 754 标准将 binary64 指定为:

符号位:1 位 指数:11 位 显着精度:53 位(52 位显式存储)

具有给定偏置指数和 52 位小数的给定 64 位双精度数据假定的实数值是

感谢 @a_guest 向我指出这一点。

【讨论】:

【参考方案10】:

其实很简单。当你有一个以 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/

【讨论】:

【参考方案11】:

我的答案很长,所以我把它分成三个部分。由于问题是关于浮点数学的,所以我把重点放在了机器的实际作用上。我还专门针对双精度(64 位)精度,但该参数同样适用于任何浮点运算。

序言

IEEE 754 double-precision binary floating-point format (binary64) 数字代表表单中的数字

值 = (-1)^s * (1.m51m50...m2m1 m0)2 * 2e-1023

64 位:

第一位是sign bit:如果数字为负数,则1,否则01。 接下来的 11 位是 exponent,即 offset 乘以 1023。换句话说,从双精度数中读取指数位后,必须减去 1023 才能获得 2 的幂。李> 剩余的 52 位是 significand(或尾数)。在尾数中,“隐含”1. 总是2 被省略,因为任何二进制值的最高有效位是 1

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

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


将双精度数转为二进制

存在许多在线转换器将双精度浮点数转换为二进制(例如binaryconvert.com),但这里有一些示例 C# 代码,用于获取双精度数的 IEEE 754 表示(我将这三个部分用冒号 (:):

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版本跳到底部)

Cato Johnston(提问者)问为什么 0.1 + 0.2 != 0.3。

以二进制形式编写(用冒号分隔三部分),值的 IEEE 754 表示形式为:

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 是:

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

两个数字相加,指数必须相同,即:

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 的形式,我们将指数加一并移动小数点 (binary) 得到:

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

现在尾数中有 53 位(第 53 位在上一行的方括号中)。 IEEE 754 的默认 rounding mode 是 'Round to Nearest' - 即如果一个数字 x 介于两个值 a 之间b,选择最低有效位为零的值。

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

注意 ab 仅在最后一位不同; ...0011 + 1 = ...0100。在这种情况下,最低有效位为零的值是b,所以和是:

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

而 0.3 的二进制表示是:

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

它与 0.1 和 0.2 之和的二进制表示仅相差 2-54

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

TL;DR

以 IEEE 754 二进制表示形式编写 0.1 + 0.2(用冒号分隔三部分)并将其与 0.3 进行比较,这是(我已将不同的位放在方括号中):

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

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

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

差异正好是 2-54,即 ~5.5511151231258 × 10-17 - 与原始值相比微不足道(对于许多应用程序而言)。

比较浮点数的最后几位本质上是危险的,因为任何阅读著名的“What Every Computer Scientist Should Know About Floating-Point Arithmetic”(涵盖此答案的所有主要部分)的人都会知道。

大多数计算器使用额外的guard digits 来解决这个问题,这就是0.1 + 0.2 给出0.3 的方式:最后几位被四舍五入。

【讨论】:

我的回答在发布后不久就被否决了。从那以后,我进行了许多更改(包括在以二进制形式写入 0.1 和 0.2 时明确指出重复出现的位,我在原始文件中省略了这些位)。如果投反对票的人看到这一点,您能否给我一些反馈,以便我改进我的答案?我觉得我的答案增加了一些新的东西,因为 IEEE 754 中对总和的处理在其他答案中没有以同样的方式涵盖。虽然“每个计算机科学家应该知道的......”涵盖了一些相同的材料,但我的回答具体涉及 0.1 + 0.2 的情况。【参考方案12】:

想象一下以 10 为基数工作,例如 8 位精度。你检查是否

1/3 + 2 / 3 == 1

并知道这会返回false。为什么?好吧,我们有实数

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

截断八位小数,我们得到

0.33333333 + 0.66666666 = 0.99999999

当然,这与 1.00000000 完全不同 0.00000001


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

1/10 = 0.0001100110011001100...(基数 2)

1/5 = 0.0011001100110011001...(基数 2)

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

0.0001100 + 0.0011001 = 0.0100101

另一方面,

3/10 = 0.01001100110011...(基数 2)

截断为 7 位的是0.0100110,它们之间的差别正好是0.0000001


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

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

【讨论】:

【参考方案13】:

不,没有损坏,但大多数小数必须是近似的

总结

浮点算术精确的,不幸的是,它与我们通常的以 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 / (2n x 5m)

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

           a / 2n

所以在十进制中,我们不能表示 1/3。因为以 10 为底包括 2 作为质因数,所以我们可以写成二进制分数的每个数字可以写成以 10 为底的分数。然而,几乎我们写为 base10 分数的任何东西都可以用二进制表示。在 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/10or 1/3.

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

处理它

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

此外,在实数运算问题(FP 是在早期的、非常昂贵的计算机上发明的问题)中,宇宙的物理常数和所有其他测量只有相对少数有效数字知道,所以无论如何,整个问题空间都是“不精确的”。 FP“准确性”在这种应用程序中不是问题。

当人们尝试使用 FP 进行豆类计数时,整个问题就真正出现了。它确实适用,但前提是你坚持使用整数值,这会破坏使用它的意义。 这就是我们拥有所有这些小数部分软件库的原因。

我喜欢 Chris 的 Pizza 回答,因为它描述了实际问题,而不仅仅是关于“不准确”的通常挥手致意。如果 FP 只是“不准确”,我们可以修复这个问题,并且在几十年前就已经做到了。我们没有这样做的原因是因为 FP 格式紧凑且快速,它是处理大量数字的最佳方式。此外,它是太空时代和军备竞赛以及早期尝试使用小内存系统解决非常慢的计算机的大问题的遗产。 (有时,单个 磁芯 用于 1 位存储,但那是 another story.)

结论

如果您只是在银行数豆子,那么首先使用十进制字符串表示的软件解决方案非常有效。但是你不能那样做量子色动力学或空气动力学。

【讨论】:

四舍五入到最接近的整数并不是在所有情况下解决比较问题的安全方法。 0.4999998 和 0.500001 舍入为不同的整数,因此每个舍入切点周围都有一个“危险区”。 (我知道那些十进制字符串可能不能完全表示为 IEEE 二进制浮点数。) 此外,尽管浮点是一种“传统”格式,但它的设计非常好。如果现在重新设计它,我不知道任何人会改变它。我对它了解得越多,我就越觉得它的设计真的很好。例如有偏差的指数意味着连续的二进制浮点数具有连续的整数表示,因此您可以在 IEEE 浮点数的二进制表示上以整数递增或递减来实现 nextafter()。此外,您可以将浮点数作为整数进行比较并得到正确答案,除非它们都是负数(因为符号幅度与 2 的补码)。 我不同意,浮点数应该存储为小数而不是二进制,所有问题都解决了。 不应“x / (2^n + 5^n)”为“x / (2^n * 5^n)"? @stephen c 您将能够在编译器设置中定义所需的精度。但它只会对结果进行四舍五入,就像在计算器中一样。【参考方案14】:

硬件设计师的视角

我认为我应该为此添加硬件设计师的观点,因为我设计和构建浮点硬件。了解错误的来源可能有助于了解软件中发生的情况,最终,我希望这有助于解释浮点错误发生的原因,并且似乎会随着时间的推移而累积。

1。概述

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

2。标准

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

在 IEEE-754 标准中,允许硬件设计人员使用任何 error/epsilon 值,只要它小于最后一个单位的二分之一,并且结果只需小于一个单位的二分之一一次手术的最后一位。这就解释了为什么当有重复操作时,错误会加起来。对于 IEEE-754 双精度,这是第 54 位,因为 53 位用于表示浮点数的数字部分(归一化),也称为尾数(例如 5.3e5 中的 5.3)。下一节将更详细地介绍各种浮点运算导致硬件错误的原因。

3。除法舍入误差的原因

浮点除法错误的主要原因是用于计算商的除法算法。大多数计算机系统使用逆乘法计算除法,主要在Z=X/YZ = X * (1/Y)。除法是迭代计算的,即每个周期计算商的一些位,直到达到所需的精度,对于 IEEE-754 来说,这是最后一位误差小于一个单位的任何东西。 Y的倒数表(1/Y)在慢除法中称为商选择表(QST),商选择表的位大小通常是基数的宽度,或位数在每次迭代中计算的商,加上一些保护位。对于 IEEE-754 标准,双精度(64 位),它将是除法器的基数的大小,加上几个保护位 k,其中k&gt;=2。因此,例如,一次计算 2 位商(基数 4)的除法器的典型商选择表将是 2+2= 4 位(加上一些可选位)。

3.1 除法舍入误差:倒数的近似

商选择表中的倒数取决于division method:慢除法如 SRT 除法,或快速除法如 Goldschmidt 除法;每个条目都根据除法算法进行修改,以尝试产生尽可能低的错误。但是,无论如何,所有倒数都是实际倒数的近似值,并引入了一些误差因素。慢除法和快除法都迭代计算商,即每一步计算商的一些位数,然后从被除数中减去结果,除法器重复这些步骤,直到误差小于一的二分之一单位排在最后。慢除法方法在每个步骤中计算商的固定位数并且通常构建成本较低,而快速除法方法计算每个步骤的可变位数并且通常构建成本更高。除法方法最重要的部分是它们中的大多数依赖于重复乘以一个倒数的近似,因此它们容易出错。

4。其他运算中的舍入误差:截断

所有运算中舍入错误的另一个原因是 IEEE-754 允许的最终答案截断的不同模式。有截断、向零舍入、round-to-nearest (default), 向下舍入和向上舍入。所有方法都会在单个操作的最后一个位置引入小于一个单位的误差元素。随着时间的推移和重复的操作,截断也会累积地增加结果错误。这种截断误差在求幂中尤其成问题,它涉及某种形式的重复乘法。

5。重复操作

由于进行浮点计算的硬件对于单次运算只需要在最后一个位置产生一个误差小于半个单位的结果,如果不注意,误差会随着重复运算而增长。这就是在需要有界误差的计算中,数学家使用诸如使用 IEEE-754 的舍入到最近的even digit in the last place 等方法的原因,因为随着时间的推移,错误更有可能相互抵消,并且Interval Arithmetic 与 IEEE 754 rounding modes 的变体相结合,以预测舍入误差并纠正它们。由于与其他舍入模式相比,其相对误差较低,因此舍入到最接近的偶数(最后一位)是 IEEE-754 的默认舍入模式。

请注意,默认的舍入模式,舍入到最近的even digit in the last place,保证一次操作的最后一位的误差小于一个单位的二分之一。单独使用截断、向上舍入和向下舍入可能会导致错误大于最后一位单位的二分之一,但小于最后一位单位,因此不建议使用这些模式,除非它们是用于区间算术。

6。总结

简而言之,浮点运算出错的根本原因是硬件截断和除法中倒数截断的结合。由于 IEEE-754 标准对单次运算只要求最后一位的误差不超过一个单位的二分之一,因此重复运算的浮点误差会累加起来,除非更正。

【讨论】:

(3) 是错误的。除法的舍入误差不小于倒数一个个单位,但最多一半倒数一个单位。 @gasher729 很好。使用默认的 IEEE 舍入模式,大多数基本操作在最后一位的误差也小于一个单位的 1/2。编辑了解释,还注意到如果用户覆盖默认的舍入模式,错误可能大于 1 ulp 的 1/2 但小于 1 ulp(在嵌入式系统中尤其如此)。 (1) 浮点数字没有错误。每个浮点值都是它的本来面目。大多数(但不是全部)浮点 操作 给出不精确的结果。例如,不存在完全等于 1.0/10.0 的二进制浮点值。另一方面,某些操作(例如 1.0 + 1.0)确实会给出准确的结果。 “浮点除法错误的主要原因是用于计算商的除法算法”是一个非常误导性的说法。对于符合 IEEE-754 的除法,浮点除法错误的唯一原因是结果无法以结果格式精确表示;无论使用何种算法,都会计算出相同的结果。 @Matt 抱歉回复晚了。这基本上是由于资源/时间问题和权衡。有一种方法可以进行长除法/更“正常”的除法,它称为 SRT 除法,基数为 2。但是,这会重复移动并从被除数中减去除数,并且需要许多时钟周期,因为它只计算每个时钟周期的商的一位。我们使用倒数表,以便我们可以计算每个周期的商的更多位,并做出有效的性能/速度权衡。【参考方案15】:

由于这个线程有点分支到对当前浮点实现的一般性讨论,我想补充一下,有一些项目可以解决他们的问题。

以https://posithub.org/ 为例,它展示了一种称为 posit 的数字类型(及其前身 unum),它有望以更少的位数提供更高的准确性。如果我的理解是正确的,它也解决了问题中的那种问题。相当有趣的项目,背后的人是数学家Dr. John Gustafson。整个东西都是开源的,在 C/C++、Python、Julia 和 C# (https://hastlayer.com/arithmetics) 中有许多实际的实现。

【讨论】:

【参考方案16】:

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

以三进制数数,但三分之二不是问题 - 也许一些每只手有 15 个手指的比赛会问为什么你的十进制数学被破坏了......

【讨论】:

由于人类使用十进制数字,我认为没有充分的理由默认浮点数不表示为小数,因此我们有准确的结果。 人类使用除以 10 为基数(十进制)以外的许多基数,二进制是我们最常用于计算的基数。“充分的理由”是您根本无法表示每个基数中的每个分数。. @RonenFestinger 二进制算术很容易在计算机上实现,因为它只需要八种带有数字的基本操作:比如 $a$, $b$ in $0,1$ 你只需要知道 $\operatorname xor(a,b)$ 和 $\operatornamecb(a,b)$,其中 xor 是异或,cb 是“进位”,在所有情况下都是 $0$,$a=1= 除外b$,在这种情况下我们有一个(实际上所有操作的交换性为您节省了 $2$ 的情况,而您所需要的只是 $6$ 的规则)。十进制扩展需要存储 10 美元乘以 11 美元(十进制表示法)的情况,每个位需要 10 美元的不同状态,并且在进位上浪费存储空间。 @RonenFestinger - 十进制并不更准确。这就是这个答案的意思。对于您选择的任何基数,都会有有理数(分数)给出无限重复的数字序列。根据记录,一些第一台计算机确实使用以 10 为基数的数字表示,但开创性的计算机硬件设计人员很快得出结论,以 2 为基数的实现更容易且更有效。【参考方案17】:

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

如果计算机以 10 为基数工作,0.1 将是 1 x 10⁻¹0.2 将是 2 x 10⁻¹0.3 将是 3 x 10⁻¹。整数运算既简单又精确,因此添加0.1 + 0.2 显然会得到0.3

计算机通常不以 10 为基数工作,它们以 2 为基数工作。您仍然可以获得某些值的精确结果,例如 0.51 x 2⁻¹0.251 x 2⁻²,然后添加它们结果为3 x 2⁻²0.75。没错。

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

【讨论】:

@Mark 谢谢你的清晰解释,但问题是为什么 0.1+0.4 正好加起来等于 0.5(至少在 Python 3 中)。在 Python 3 中使用浮点数时检查相等性的最佳方法是什么? @user2417881 IEEE 浮点运算对每个运算都有舍入规则,有时即使两个数字相差一点,舍入也能产生准确的结果。细节太长了,无法发表评论,而且我也不是这方面的专家。正如您在此答案中看到的那样,0.5 是可以用二进制表示的少数小数之一,但这只是巧合。有关相等性测试,请参阅***.com/questions/5595425/…。 @user2417881 你的问题让我很感兴趣,所以我把它变成了一个完整的问答:***.com/q/48374522/5987【参考方案18】:

已经发布了很多好的答案,但我想再附加一个。

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

在后台存储实数的模型将浮点数表示为

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

所以要准确地表示这些数字有点困难。即使您明确指定此变量而无需任何中间计算。

【讨论】:

【参考方案19】:

只是为了好玩,我按照标准 C99 中的定义玩弄了浮点数的表示,并编写了下面的代码。

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

SIGN EXPONENT FRACTION

然后它打印一个总和,当以足够的精度求和时,它将显示硬件中真正存在的值。

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

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

在代码之后,我附加了一个控制台会话,在该会话中,我计算了硬件中真正存在的由编译器插入的两个常量(减去 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 中设置scale 因子。

显示的总和是硬件内部的。您通过计算获得的值取决于您设置的比例。我确实将 scale 因子设置为 15。从数学上讲,无限精确,它似乎是 1,000,000,000。

【讨论】:

【参考方案20】:

浮点舍入错误。来自What Every Computer Scientist Should Know About Floating-Point Arithmetic:

将无限多个实数压缩为有限位数需要近似表示。尽管整数有无限多,但在大多数程序中,整数计算的结果可以存储在 32 位中。相反,给定任何固定位数,大多数实数计算将产生无法使用那么多位精确表示的量。因此,浮点计算的结果必须经常四舍五入以适应其有限表示。这种舍入误差是浮点计算的特征。

【讨论】:

【参考方案21】:

另一种看待这个问题的方式:使用 64 位来表示数字。因此,无法精确表示超过 2**64 = 18,446,744,073,709,551,616 个不同的数字。

但是,Math 表示 0 和 1 之间已经有无数个小数。IEE 754 定义了一种编码,可以有效地使用这 64 位来获得更大的数字空间以及 NaN 和 +/- Infinity,因此准确表示之间存在差距用数字填充的数字只是近似值。

不幸的是,0.3 处于差距。

【讨论】:

【参考方案22】:

这个问题的许多重复项中的许多都询问浮点舍入对特定数字的影响。在实践中,通过查看感兴趣的计算的确切结果而不是仅仅阅读它更容易了解它是如何工作的。某些语言提供了这样做的方法 - 例如在 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.300000000000000004 的等效值也会给出舍入误差 0.0000000000000000277555756156289135105907917022705078125。采用四舍五入的决胜局。

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

【讨论】:

致我刚刚回滚编辑的人:我认为代码引用适合引用代码。这个答案与语言无关,根本不包含任何引用的代码。数字可以用在英语句子中,不会把它们变成代码。 This 可能是为什么有人将您的数字格式化为代码 - 不是为了格式化,而是为了可读性。 ... 另外,round to even 指的是 binary 表示,not decimal 表示。请参阅this 或例如this。 @WaiHaLee 我没有将奇数/偶数测试应用于任何十进制数,仅应用于十六进制数。当且仅当其二进制扩展的最低有效位为零时,十六进制数字是偶数。【参考方案23】:

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

当使用 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%)。

【讨论】:

【参考方案24】:

可以在数字计算机中实现的浮点数学运算必然使用实数的近似值并对其进行运算。 (标准版本有超过五十页的文档,并有一个委员会来处理它的勘误和进一步完善。)

这种近似值是不同种类的近似值的混合,由于其与精确度的特定偏差方式,每种近似值都可以忽略或仔细考虑。它还涉及许多硬件和软件级别的明显异常情况,大多数人会假装没有注意到。

如果您需要无限精度(例如,使用数字 π,而不是其众多较短的替代项之一),您应该编写或使用符号数学程序。

但是,如果您认为有时浮点数学在值和逻辑上是模糊的,并且错误会迅速累积,并且您可以编写您的需求和测试以实现这一点,那么您的代码通常可以通过使用您的 FPU 中的内容。

【讨论】:

【参考方案25】:

鉴于没有人提到这一点......

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

Python 的decimal module 和Java 的BigDecimal class,在内部用十进制表示法(与二进制表示法相反)表示数字。两者都具有有限的精度,因此它们仍然容易出错,但是它们解决了二进制浮点运算的大多数常见问题。

在处理金钱时,小数非常好:10 美分加 20 美分总是正好是 30 美分:

>>> 0.1 + 0.2 == 0.3
False
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True

Python 的decimal 模块基于IEEE standard 854-1987。

Python 的 fractions module 和 Apache Common 的 BigFraction class。两者都将有理数表示为(numerator, denominator) 对,并且它们可能给出比十进制浮点运算更准确的结果。

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

【讨论】:

我们也可以使用定点。例如,如果美分是您最精细的粒度,则可以使用美分而不是美元的整数来进行计算。【参考方案26】:

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

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

那个比萨刀的动作非常精细,如果你从一整块比萨开始,然后将其减半,然后每次将最小的切片减半,你可以在切片前减半53次就连它的高精度能力都太小了。那时,您不能再将那个非常薄的切片减半,而必须按原样包含或排除它。

现在,您将如何将所有切片拼凑起来,使之加起来相当于披萨的十分之一 (0.1) 或五分之一 (0.2)?认真想想,然后努力解决。如果您手头有一个神话般的精密比萨刀,您甚至可以尝试使用真正的比萨饼。 :-)


当然,最有经验的程序员都知道真正的答案,那就是,无论切片多么精细,都无法使用这些切片将披萨的十分之一或五分之一精确拼凑起来他们。你可以做一个很好的近似值,如果你将 0.1 的近似值与 0.2 的近似值相加,你会得到一个很好的近似值 0.3,但它仍然只是一个近似值。

有关双精度数(它是允许您减半您的比萨饼53倍的精确度),数字立即以下且大于0.1是0.09999999999999999167332731531132594682276248931884765625和0.1000000000000000055511151231257827021181583404541015625。后者比前者更接近 0.1,因此数字解析器将在输入 0.1 的情况下支持后者。

(这两个数字之间的差异是我们必须决定包含的“最小切片”,它会引入向上偏差,或者排除,它会引入向下偏差。最小切片的技术术语是@987654321 @.)

在 0.2 的情况下,数字都是相同的,只是放大了 2 倍。同样,我们更喜欢略高于 0.2 的值。

请注意,在这两种情况下,0.1 和 0.2 的近似值都有轻微的向上偏差。如果我们添加足够多的这些偏差,它们会使数字越来越远离我们想要的,事实上,在 0.1 + 0.2 的情况下,偏差足够高,结果数字不再是最接近的数字到 0.3。

在特别地,0.1 + 0.2是真的0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125,而数字最接近0.3实际上是0.299999999999999988897769753748434595763683319091796875。 P>


附:一些编程语言还提供了可以split slices into exact tenths 的披萨刀。虽然这种比萨刀并不常见,但如果您确实可以使用它,那么您应该在重要的是能够准确地获得十分之一或五分之一的切片时使用它。

(Originally posted on Quora.)

【讨论】:

请注意,有些语言包含精确数学。一个例子是 Scheme,例如通过 GNU Guile。请参阅 draketo.de/english/exact-math-to-the-rescue — 这些将数学保留为分数,最后只切片。 @FloatingRock 实际上,很少有主流编程语言内置有理数。 Arne 和我一样是一名计划者,所以这些都是我们被宠坏的东西。 @ArneBabenhauserheide 我认为值得补充的是,这只适用于有理数。因此,如果您使用 pi 等无理数进行数学运算,则必须将其存储为 pi 的倍数。当然,任何涉及 pi 的计算都不能表示为精确的十进制数。 @connexo 好的。您将如何对您的披萨旋转器进行编程以获得 36 度?什么是36度? (提示:如果你能以精确的方式定义它,你也有一个精确的十分之一的比萨刀。)换句话说,你实际上不可能有 1/360(度数)或 1/ 10(36 度),只有二进制浮点数。 @connexo 另外,“每个白痴”都无法将披萨准确地旋转 36 度。人类太容易出错,无法做如此精确的事情。【参考方案27】:

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

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

【讨论】:

@Nae 我会将第二段翻译为“大多数分数不能用十进制 二进制精确表示。所以大多数结果将被四舍五入——尽管它们仍将精确到正在使用的表示中固有的位数/位数。”【参考方案28】:

您尝试过胶带解决方案吗?

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

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

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

【讨论】:

【参考方案29】:

我的解决方法:

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

精度是指加法时要保留小数点后的位数。

【讨论】:

【参考方案30】:

除了其他正确答案之外,您可能还需要考虑缩放值以避免浮点运算问题。

例如:

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

...而不是:

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

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

作为一个实际示例,为避免精度至关重要的浮点问题,建议1将货币处理为表示美分数量的整数:2550cent 而不是 @987654327 @美元。


1 道格拉斯·克罗克福德:JavaScript: The Good Parts: Appendix A - Awful Parts (page 105)。

【讨论】:

问题是转换本身不准确。 16.08 * 100 = 1607.9999999999998。我们是否必须求助于拆分数字并单独转换(如 16 * 100 + 08 = 1608)? 这里的解决方案是以整数进行所有计算,然后除以您的比例(在本例中为 100),仅在呈现数据时进行舍入。这将确保您的计算始终准确。 只是挑剔一点:整数运算仅在浮点数中精确到一个点(双关语)。如果该数字大于 0x1p53(使用 Java 7 的十六进制浮点表示法,= 9007199254740992),则此时 ulp 为 2,因此 0x1p53 + 1 向下舍入为 0x1p53(并且 0x1p53 + 3 向上舍入为 0x1p53 + 4、由于四舍五入)。 :-D 但是当然,如​​果你的数字小于 9 万亿,你应该没问题。 :-P

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

python浮点数是啥意思

php 数学函数

python中1.0是浮点数还是整数

打乱一个浮点数?

TCL:变量存储一个代表浮点数的十六进制值,如何将它作为浮点数显示在屏幕上?

Classifier.fit for oneclassSVM 抱怨浮点类型。 TypeError 浮点数是必需的