7

考虑以下 C# 代码:

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

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

result1 应该总是等于 result2 对吧?问题是,它没有。结果 1 为 3.3,结果 2 为 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 设计的博士学位,但我只是想我会问。

编辑 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 
}

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

4

7 回答 7

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 // 在求和期间扩展
r1 = 1.0 + 1.1 = 10.0001100110 // 截断为 12 位
r1 + 1.2 = 11.01001100101 // 在求和期间扩展
r2 = r1 + 1.2 = 11.0100110010 // 截断为 12 位

1.1 + 1.2 = 10.01001100110 // 在求和期间扩展
r3 = 1.1 + 1.2 = 10.0100110011 // 截断为 12 位
r3 + 1.0 = 11.01001100110 // 在求和期间扩展
r4 = r3 + 1.0 = 11.0100110011 // 截断为 12 位

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

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

在另一种排序中,四舍五入删除了两次尾随0

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

双精度位的精度更高,C# 可能使用舍入而不是截断,但希望这个简单的模型向您展示相同值的不同排序可能会发生不同的错误。

fp 和 maths 之间的区别在于 + 是“add then round”的简写,而不仅仅是 add。

于 2009-03-30T13:31:45.093 回答
6

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

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

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

于 2009-03-30T11:09:15.403 回答
4

请参阅有关该主题的经典论文(每个计算机科学家都应该了解的关于浮点运算的知识)。浮点运算会发生这种情况。需要计算机科学家来告诉你 1/3+1/3+1/3等于 1...

于 2009-03-30T11:18:32.667 回答
2

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

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

这可能很有趣:每个计算机科学家都应该知道的关于浮点运算的知识

于 2009-03-30T11:08:46.013 回答
1

result1 应该总是等于 result2 对吧?

错了。这在数学中是正确的,但在浮点算术中则不然。

您需要阅读一些数值分析入门

于 2009-03-30T11:16:43.700 回答
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,但不能保证。

于 2009-03-30T15:54:45.077 回答
0

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

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

因为双精度数不能准确地表示十进制值,所以您会得到不同的结果。

于 2009-03-30T11:18:05.007 回答