【问题标题】:Why does the order affect the rounding when adding multiple doubles in C#为什么在C#中添加多个双精度时顺序会影响舍入
【发布时间】:2009-03-30 11:05:47
【问题描述】:

考虑以下 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 
}

编译器正在帮我加起来!

【问题讨论】:

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


    【解决方案1】:

    我本来希望 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.1加上1.0,1.1的最后一位丢失,结果不一样。

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

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

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

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

    fp 和数学之间的区别在于 + 是“加然后舍入”的简写,而不仅仅是加。

    【讨论】:

    • 这个例子很好,但是与原始代码相比,您改变了操作的顺序。 OP 在第一次编辑中做了同样的事情。
    【解决方案2】:

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

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

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

    【讨论】:

    • 但是如果 A+B 产生舍入误差而 A+C 不产生,那么 (A+C)+B 不还是和 A+B 有同样的舍入误差吗?我得到四舍五入的错误加起来,我在问为什么顺序很重要。
    • 否,A+B 产生错误 e1,然后 A+B +C 产生另一个错误 e2,所以最终错误是 e1+e2。如果 A+C 没有提供错误,则 A+C +B 提供单个错误 e3,它没有理由匹配 e1+e2。你应该谷歌浮点运算以获取更多信息。
    • 我想我没有说清楚,让我改写一下“如果 A+B 使 e1,(A+B)+C 使 e2,A+C 使 e3 和 (A+C) +B 生成 e4,为什么 e1+e2 不等于 e3+e4?到目前为止,您已经告诉我它不等于,但没有告诉我为什么不等于。”
    • e3 和 e4 可能为零,而 e1 和 e2 不是。
    • @dant :尝试计算 1/3*3 :由于明显的简化,结果为 1。现在尝试计算 2/3(结果是 0.6666...),加上 0.3333,结果现在是 0.999999...而不是 1)。同样的推理也适用于浮点运算。维基百科上有很好的解释(见下面的链接)
    【解决方案3】:

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

    【讨论】:

    • 我承认浮点运算让我害怕。根据经验,除非必须,否则我拒绝使用它。
    • 在有用时使用它们:近似值可以。物理模拟、外部获取的测量值、图形布局(用于不同级别的允许近似值)。钱——没那么多!没有任何工具如此简单,你不可能用它伤到自己……(嗯,致命的镊子事故?)。
    【解决方案4】:

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

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

    这可能很有趣:What Every Computer Scientist Should Know About Floating-Point Arithmetic

    【讨论】:

      【解决方案5】:

      result1 应该总是等于 result2 对吧?

      错误。这在数学中是正确的,但在 floating-point arithmetics 中不是。

      您需要阅读一些Numerical Analysis primer

      【讨论】:

        【解决方案6】:

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

        假设对于 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,但没有保证。

        【讨论】:

          【解决方案7】:

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

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

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

          【讨论】:

            猜你喜欢
            • 2011-01-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-05-11
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多