这个问题措辞令人困惑。让我们把它分解成许多小问题:
为什么在浮点运算中十分之一加十分之二并不总是等于十分之三?
让我给你打个比方。假设我们有一个数学系统,其中所有数字都精确到小数点后五位。假设你说:
x = 1.00000 / 3.00000;
您会期望 x 为 0.33333,对吗?因为这是我们系统中最接近真实答案的数字。现在假设你说
y = 2.00000 / 3.00000;
您会期望 y 为 0.66667,对吗?因为这也是我们系统中最接近真实答案的数字。0.66666比 0.66667 离三分之二更远。
请注意,在第一种情况下,我们向下取整,在第二种情况下,我们向上取整。
现在当我们说
q = x + x + x + x;
r = y + x + x;
s = y + y;
我们得到什么?如果我们进行精确的算术运算,那么这些显然都是三分之四,而且它们都是相等的。但它们并不相等。尽管 1.33333 是我们系统中最接近三分之四的数字,但只有 r 具有该值。
q 是 1.33332——因为 x 有点小,每次加法都会累积该错误,最终结果有点太小了。同样,s 太大;它是 1.33334,因为 y 有点太大了。r 得到了正确的答案,因为 y 的过大被 x 的过小抵消,结果最终是正确的。
精度的位数对误差的大小和方向有影响吗?
是的; 更高的精度使误差的幅度更小,但可以改变计算是由于误差而产生的损失或收益。例如:
b = 4.00000 / 7.00000;
b 将是 0.57143,它从 0.571428571 的真实值向上四舍五入......如果我们去了 8 个地方,那就是 0.57142857,它的误差幅度要小得多,但方向相反;它四舍五入。
因为改变精度可以改变每个单独计算中的误差是收益还是损失,这可以改变给定聚合计算的误差是相互加强还是相互抵消。最终结果是,有时较低精度的计算比高精度计算更接近“真实”结果,因为在较低精度的计算中你很幸运,而且误差在不同的方向。
我们希望以更高的精度进行计算总是给出更接近真实答案的答案,但这个论点表明并非如此。这就解释了为什么有时浮点数的计算给出了“正确”的答案,而双精度数的计算——精度是两倍——给出了“错误”的答案,对吗?
是的,这正是您的示例中发生的情况,除了我们有一定数量的二进制精度而不是五位十进制精度。正如三分之一不能用五位或任何有限的十进制数字准确表示一样,0.1、0.2 和 0.3 也不能用任何有限的二进制数字准确表示。其中一些将向上舍入,其中一些将向下舍入,并且它们的添加是否会增加误差或消除误差取决于每个系统中有多少二进制数字的具体细节。也就是说,精度的变化可以改变答案无论好坏。通常,精度越高,答案越接近真实答案,但并非总是如此。
如果 float 和 double 使用二进制数字,我怎样才能获得准确的十进制算术计算?
如果您需要精确的十进制数学,请使用decimal
类型;它使用十进制分数,而不是二进制分数。你付出的代价是它更大更慢。当然,正如我们已经看到的,像三分之一或七分之四这样的分数不会被准确地表示。然而,实际上是小数的任何分数都将用零错误表示,最多约 29 位有效数字。
好的,我接受所有浮点方案由于表示错误而引入不准确性,并且这些不准确性有时会根据计算中使用的精度位数而累积或相互抵消。我们是否至少可以保证这些不准确之处是一致的?
不,你对浮点数或双打没有这样的保证。编译器和运行时都被允许以比规范要求更高的精度执行浮点计算。特别是,允许编译器和运行时以 64 位或 80 位或 128 位或任何大于32 的位数进行单精度(32 位)算术。
允许编译器和运行时这样做,但他们当时觉得这样。它们不需要在机器之间、从运行到运行等等保持一致。由于这只能使计算更准确,因此这不被视为错误。这是一个特点。这个特性使得编写行为可预测的程序变得异常困难,但仍然是一个特性。
所以这意味着在编译时执行的计算,如文字 0.1 + 0.2,可以给出与在运行时使用变量执行的相同计算不同的结果?
是的。
比较 to 的结果怎么0.1 + 0.2 == 0.3
样(0.1 + 0.2).Equals(0.3)
?
由于第一个是由编译器计算的,而第二个是由运行时计算的,我只是说他们可以随意使用比规范要求的精度更高的精度,是的,这些可能会给出不同的结果。也许其中一个选择仅以 64 位精度进行计算,而另一个选择 80 位或 128 位精度进行部分或全部计算并得到不同的答案。
所以在这里等一下。你说的不仅0.1 + 0.2 == 0.3
可以不同于(0.1 + 0.2).Equals(0.3)
. 您是说0.1 + 0.2 == 0.3
可以完全根据编译器的心血来潮计算为真或假。它可以在星期二产生真,在星期四产生假,它可以在一台机器上产生真而在另一台机器上产生假,如果表达式在同一个程序中出现两次,它可以产生真假。无论出于何种原因,此表达式都可以具有任一值;这里允许编译器完全不可靠。
正确的。
这通常向 C# 编译器团队报告的方式是,有人有一些表达式,当他们在调试模式下编译时产生 true,而在发布模式下编译时产生 false。这是最常见的情况,因为调试和发布代码生成更改了寄存器分配方案。但是编译器可以对这个表达式做任何它喜欢的事情,只要它选择真或假。(比如说,它不能产生编译时错误。)
这是疯狂。
正确的。
我应该为这个烂摊子怪谁?
不是我,那是肯定的。
英特尔决定制造一种浮点数学芯片,在这种芯片中,要获得一致的结果要昂贵得多。编译器中关于注册哪些操作与保留在堆栈上的操作的小选择可能会导致结果的巨大差异。
如何确保一致的结果?
decimal
正如我之前所说,使用类型。或者用整数做所有的数学运算。
我必须使用双打或花车;我可以做些什么来鼓励一致的结果吗?
是的。如果您将任何结果存储到任何静态字段、类的任何实例字段或float 或 double 类型的数组元素中,则可以保证将其截断回 32 位或 64 位精度。(此保证明确不适用于存储到本地或形式参数。)此外,如果您对已经属于该类型的表达式执行运行时强制转换(float)
,(double)
则编译器将发出特殊代码,强制截断结果,就好像它已分配给字段或数组元素。(在编译时执行的强制转换——即对常量表达式的强制转换——不能保证这样做。)
为了澄清最后一点:C#语言规范是否做出了这些保证?
不会。运行时保证存储到数组或字段中会被截断。C# 规范不保证身份转换会被截断,但 Microsoft 实现具有回归测试,可确保编译器的每个新版本都具有此行为。
关于这个主题的所有语言规范都必须说的是,浮点运算可以根据实现的判断以更高的精度执行。