为什么在C#中添加多个双精度时,顺序会影响舍入?

时间:2009-03-30 11:05:47

标签: c# .net compiler-construction rounding precision

考虑以下C#代码:

double result1 = 1.0 + 1.1 + 1.2;
double result2 = 1.2 + 1.0 + 1.1;

if (result1 == result2)
{
    ...
}

result1应该始终等于result2对吗?问题是,事实并非如此。 result1是3.3,result2是3.3000000000000003。唯一的区别是常数的顺序。

我知道双打是以可能发生舍入问题的方式实现的。我知道如果我需要绝对精度,我可以使用小数。或者我可以在if语句中使用Math.Round()。我只是一个想要了解C#编译器正在做什么的书呆子。有谁能告诉我?

修改

感谢大家到目前为止建议阅读浮点运算和/或谈论CPU如何处理双精度的固有不准确性。但我觉得我的问题的主旨仍然没有答案。因为没有正确地说出来,这是我的错。让我这样说:

分解上面的代码,我希望发生以下操作:

double r1 = 1.1 + 1.2;
double r2 = 1.0 + r1
double r3 = 1.0 + 1.1
double r4 = 1.2 + r3

假设上述每个加法都有一个舍入误差(编号为e1..e4)。所以r1包含舍入误差e1,r2包括舍入误差e1 + e2,r3包含e3,r4包含e3 + e4。

现在,我不知道舍入错误究竟是如何发生的,但我希望e1 + e2等于e3 + e4。显然它没有,但这对我来说似乎有些不对劲。另一件事是,当我运行上面的代码时,我没有得到任何舍入错误。这就是让我觉得C#编译器正在做一些奇怪而不是CPU的事情。

我知道我问了很多,也许任何人都可以给出的最佳答案是去做CPU设计的PHD,但我只是想我会问。

修改2

从原始代码示例中查看IL,很明显编译器不是CPU正在执行此操作:

.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
    .maxstack 1
    .locals init (
        [0] float64 result1,
        [1] float64 result2)
    L_0000: nop 
    L_0001: ldc.r8 3.3
    L_000a: stloc.0 
    L_000b: ldc.r8 3.3000000000000003
    L_0014: stloc.1 
    L_0015: ret 
}

编译器正在为我添加数字!

7 个答案:

答案 0 :(得分:10)

  

我原本期望e1 + e2等于e3 + e4。

这与期待

完全不同
 floor( 5/3 ) + floor( 2/3 + 1 )

等于

 floor( 5/3 + 2/3 ) + floor( 1 )

除了你在发言前乘以2 ^ 53。

使用12位精度浮点和截断值:

1.0            =  1.00000000000
1.1            =  1.00011001100
1.2            =  1.00110011001

1.0 + 1.1      = 10.00011001100 // extended during sum
r1 = 1.0 + 1.1 = 10.0001100110  // truncated to 12 bit
r1  + 1.2      = 11.01001100101 // extended during sum
r2 = r1  + 1.2 = 11.0100110010  // truncated to 12 bit

1.1 + 1.2      = 10.01001100110 // extended during sum
r3 = 1.1 + 1.2 = 10.0100110011  // truncated to 12 bit
r3 + 1.0       = 11.01001100110 // extended during sum
r4 = r3  + 1.0 = 11.0100110011  // truncated to 12 bit

因此,更改操作/截断的顺序会导致错误更改,并且r4!= r2。如果在此系统中添加1.1和1.2,则最后一位会携带,因此在截断时不会丢失。如果将1.0添加到1.1,则1.1的最后一位将丢失,因此结果不一样。

在一个排序中,舍入(通过截断)删除尾随1

在另一个排序中,舍入会两次删除尾随0

一个不等于零;所以错误不一样。

双打有更多的精度,而C#可能使用舍入而不是截断,但希望这个简单的模型能够显示不同的错误可能会发生在相同值的不同排序上。

fp和数学之间的区别在于+是'add then round'的简写,而不仅仅是添加。

答案 1 :(得分:6)

c#编译器没有做任何事情。 CPU是。

如果在CPU寄存器中有A,然后添加B,则存储在该寄存器中的结果为A + B,近似于使用的浮点精度

如果然后添加C,则错误加起来。此错误添加不是传递操作,因此是最终的差异。

答案 2 :(得分:4)

有关此主题,请参阅the classic paper (What every computer scientist should know about floating-point arithmetic)。浮点运算会发生这种情况。需要一位计算机科学家告诉你,1/3 + 1/3 + 1/3 不是等于1 ......

答案 3 :(得分:2)

浮点运算的顺序很重要。不直接回答您的问题,但您应该始终小心比较浮点数。通常包括公差:

double epsilon = 0.0000001;
if (abs(result1 - result2) <= epsilon)
{
    ...
}

这可能是有意义的:What Every Computer Scientist Should Know About Floating-Point Arithmetic

答案 4 :(得分:1)

  

result1应始终等于result2   正确?

<强>错误即可。这在数学中是正确的,但在floating-point arithmetics中没有。

您需要阅读一些Numerical Analysis primer

答案 5 :(得分:1)

为什么错误不同,取决于顺序可以用不同的例子来解释。

让我们说对于10以下的数字,它可以存储所有数字,因此它可以存储1,2,3等等,直到包括10,但在10之后,它只能存储每秒数,到期内部精度损失,换句话说,它只能存储10,12,14等。

现在,通过该示例,您将了解以下为什么会产生不同的结果:

1 + 1 + 1 + 10 = 12 (or 14, depending on rounding)
10 + 1 + 1 + 1 = 10

浮点数的问题在于它们无法准确表示,并且错误并不总是相同,所以顺序很重要。

例如,3.00000000003 + 3.00000000003可能最终为6.00000000005(最后通知不是6),但3.00000000003 + 2.99999999997可能最终为6.00000000001,并且具有:

step 1: 3.00000000003 + 3.00000000003 = 6.00000000005
step 2: 6.00000000005 + 2.99999999997 = 9.00000000002

但是,请更改顺序:

step 1: 3.00000000003 + 2.99999999997 = 6.00000000001
step 2: 6.00000000001 + 3.00000000003 = 9.00000000004

所以这很重要。

现在,当然,你可能会幸运的是,上面的例子相互平衡,第一个将由.xxx1向上摆动而另一个由.xxx1向下摆动,两者都给你.xxx3,但是没有保证。

答案 6 :(得分:0)

您实际上没有使用相同的值,因为中间结果不同:

double result1 = 2.1 + 1.2;
double result2 = 2.2 + 1.1;

因为双精度不能完全代表十进制值,所以会得到不同的结果。