这个问题措辞混乱。让我们将其分解为许多较小的问题:
为什么在浮点运算中十分之一加十分之二并不总是等于十分之三?
让我给你一个比喻。假设我们有一个数学系统,其中所有数字均四舍五入到小数点后五个位。假设您说:
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向上取整...如果我们转到0.57142857的八个位置,则误差的大小要小得多,但方向相反;四舍五入。
因为更改精度可以更改每个单独计算中的误差是收益还是损失,所以这可以更改给定聚合计算的误差是相互补充还是相互抵消。最终结果是,有时精度较低的计算要比精度较高的计算更接近“真实”结果,因为在精度较低的计算中,您会很幸运,并且错误方向不同。
我们希望以更高的精度进行计算总会得到一个更接近于真实答案的答案,但是该论点反而表明了这一点。这就解释了为什么有时用浮点数进行计算会给出“正确”的答案,而使用双精度数(具有两倍的精度)却给出“错误”的答案,对吗?
是的,这正是您的示例中所发生的,只是我们有一定数量的二进制精度,而不是十进制精度的五位数字。正如不能以五位或任何有限数目的十进制数字精确表示三分之一一样,也不能以任何有限数目的二进制数精确地表示0.1、0.2和0.3。其中一些将被四舍五入,其中一些将被四舍五入,添加它们是否增加错误或消除错误取决于每个系统中有多少个二进制数字的具体细节。也就是说,精度的改变可以改变答案无论好坏。通常,精度越高,答案越接近真实答案,但并不总是如此。
如果float和double使用二进制数字,那么如何获得准确的十进制算术运算呢?
如果您需要精确的十进制数学,则使用decimal
类型;它使用十进制分数,而不是二进制分数。您所付出的代价是它更大且更慢。当然,正如我们已经看到的那样,分数或三分之一之类的分数将无法准确表示。但是,实际上是十进制小数的任何小数都将以零误差表示,最多约29个有效数字。
好的,我接受所有浮点方案由于表示错误而引入的不准确性,并且这些不准确性有时会根据计算中使用的精度位数相互累积或抵消。我们至少可以保证这些错误是一致的吗?
不,您没有浮动或双倍保证金。允许编译器和运行时以比规范要求更高的精度执行浮点计算。特别是,允许编译器和运行时以64位或80位或128位或任何喜欢的大于32的位进行单精度(32位)算术。
允许编译器和运行时这样做,但是当时感觉很不错。它们不必在机器之间,每次运行之间保持一致,等等。由于这只能使计算更准确,因此不将其视为错误。这是一个功能。这项功能使编写具有可预测行为的程序变得异常困难,但这是一项功能。
因此,这意味着在编译时执行的计算(如字面量0.1 + 0.2)会比在运行时使用变量进行的相同计算产生不同的结果吗?
是的
比较0.1 + 0.2 == 0.3
to的结果(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
可以完全根据编译器的想法将其计算为true或false。它可以在星期二产生true,在星期四产生false,可以在一台机器上产生true,而在另一台机器上产生false,如果该表达式在同一程序中出现两次,则可以产生true和false。无论出于任何原因,此表达式都可以具有值;此处允许编译器完全不可靠。
正确。
通常将这种情况报告给C#编译器团队的方式是,某人具有一些表达式,这些表达式在调试时进行编译,而在发布模式下进行编译时则生成false。这是最常见的情况,因为调试和发布代码生成会更改寄存器分配方案。但是编译器允许做这种表达任何它喜欢的,只要它选择真或假。(例如,它不能产生编译时错误。)
这是疯狂。
正确。
我应该为这个烂摊子指责谁?
不是我,那是肯定的。
英特尔决定制造一种浮点数学芯片,要获得一致的结果,该芯片要昂贵得多。编译器中关于要注册的操作与要保留在堆栈上的操作的微小选择可能会导致结果大相径庭。
如何确保结果一致?
decimal
就像我之前说的那样使用类型。或以整数形式进行所有数学运算。
我必须使用双精度或浮点数;我可以做些什么来鼓励一致的结果吗?
是。如果将任何结果存储到任何静态字段,类型为float或double的类或数组元素的任何实例字段中,则可以保证将其截断为32或64位精度。(这保证明确不为加盟店当地人或正式的参数进行。)此外,如果你做一个运行时投来(float)
或(double)
对已经是该类型的,则编译器会发出特殊的代码表达的是力量的结果截断,就好像它已分配给字段或数组元素。(不保证在编译时执行的转换(即对常量表达式进行转换)。)
为了阐明最后一点:C#语言规范是否提供这些保证?
否。运行时保证将截断存储到数组或字段中。C#规范不保证身份转换会被截断,但是Microsoft实现具有回归测试,以确保编译器的每个新版本均具有此行为。
语言规范在这个主题上必须说的是,浮点运算可以根据实现的自由度以更高的精度执行。