【问题标题】:Accumulating errors with decimal type?累积十进制类型的错误?
【发布时间】:2013-10-15 15:07:49
【问题描述】:

我有一个应用程序可以累积十进制值(加法和减法)。为了避免累积错误,我使用小数类型而不是双精度。但是,我遇到了一个行为与我所期望的不太一样的情况。

我有 x = a + b,其中 a = 487.5M 和 b = 433.33333333333333333333333335M。

计算加法,我得到 x = 920.8333333333333333333333334M。

然后我有 y = 967.8750000000000000000000001M。

我想断言 y - x = y - a - b。然而,

y - x = 47.0416666666666666666666667

y - a - b = 47.04166666666666666666666675

我认为这种错误正是十进制类型要避免的,所以这里发生了什么?

这里是重现问题的代码:

    static void Main()
    {
        decimal a = 487.5M;
        decimal b = 433.33333333333333333333333335M;
        decimal x = a + b;

        decimal y = 967.8750000000000000000000001M;

        Console.WriteLine(y - x);
        Console.WriteLine(y - a - b);
        if (y - x != y - a - b)
            Console.WriteLine("x - y != y - a - b");
        Console.ReadKey();
    }

在 cmets 中有一些关于为什么需要这些高精度的讨论,所以我想我会在这里总结一下。出于显示目的,我当然会对这些操作的结果进行四舍五入,但我对所有内部表示都使用十进制。一些计算沿途需要分数,这会导致数字超出小数类型的精度。

不过,我会尽量保持一切稳定以备积累。因此,例如,如果我将一个数量分成三分之三,我取 x/3、x/3,然后是 (x - x/3 - x/3)。这是一个计算物理量的系统,这些物理量经常像这样被分割,所以我不想过早地通过四舍五入来引入偏差。例如,如果我将上面 x=1 的结果四舍五入到小数点后三位,我将得到 0.333、0.333、0.334 作为运算的三个部分。

系统可以执行的操作的精确度存在实际的物理限制,但理想情况下,系统尝试执行的操作的逻辑说明应该尽可能精确。主要的关键要求是系统的总量不应因这些不同的操作而改变。在上述情况下,我发现 decimal 可能违反此假设,因此我想更好地了解为什么会发生这种情况以及如何解决它。

【问题讨论】:

  • 十进制类型更准确,但也有其局限性。使用这么多小数位的确切意义是什么?这实际上是必需的,还是您只是想知道小数类型的真正限制?
  • @varocarbas,这些是由生产中的应用程序计算的实际数字。我的假设是这样的简单加减法总是适用于小数,即使它们处于精度的极限,因为加法和减法永远不需要更高的精度来表示相同基数的结果。我可以对早期操作​​的结果进行四舍五入以避免这种过分的精度,但这会引发其他过程问题(即,舍入的正确点是什么?)
  • 恐怕你的说法“为小数工作”是错误的。只要在最大允许精度范围内,任何计算都将有效。示例:在允许 3 个小数的类型中,任何涉及更多小数的运算都将被截断,例如 0.333+0.777 或 0.333*0.777;您不能期望给定类型执行超出其可以处理的最大位数的计算。
  • 关于四舍五入,从逻辑上讲,中间计算要尽量避免(因为会降低精度)。在最可能的情况下,通过考虑小数类型,您不需要对中间值进行四舍五入。即使在您的某种特殊条件下,您似乎也不必四舍五入任何中间值(只需四舍五入最终结果就足够了)。要四舍五入的小数位数取决于您的预期精度;我个人很少四舍五入超过 6 位小数(标准应用程序为 3-4),但这取决于您的要求。
  • @varocarbas,是的,这显然是乘法或除法的问题,但我不清楚为什么这是加法和减法的问题。 0.333 + 0.777 = 1.110;除非您假设它们是整数分数的截断表示,否则这里没有歧义/截断。在这种情况下,我并不担心,因为数字所代表的确切数量对我来说并不像它在积累下的稳定性那么重要。

标签: c# .net decimal rounding-error


【解决方案1】:

C# 类型 Decimal 不像 COBOL 中使用的十进制类型,它实际上每个半字节存储一个十进制数字,并且使用类似于手工进行十进制数学的数学方法。相反,它是一种浮点类型,只是假设数量不会变得那么大,因此它使用较少的位来表示指数,并使用剩余的 128 位而不是 64 位来表示双精度,从而大大提高了准确性。

但是作为浮点表示,即使是非常简单的小数值也不能精确表示:例如,0.1 需要二进制重复小数,并且可能无法存储为精确值。 (它不是,对于双精度数;十进制可能会以不同的方式处理该特定值,但通常情况下是这样。)

因此,仍然需要使用典型的浮点数学程序进行比较,在该程序中,通过仅将值接受到某个点来比较、加法、减法等。由于大约有 23 位小数精度,因此请选择 16 作为您的标准,然后忽略末尾的那些。

如需良好的参考,请阅读What Every Computer Scientist Should Know About Floating Point Precision

【讨论】:

  • 我明白了,我没有意识到小数点实际上是浮点数。这解释了结果,因为执行加法和减法没有固定的精度。这对我的应用程序来说很不幸,因为我在很多地方都完全依赖于这个假设。
【解决方案2】:

Decimal 类型是一种浮点类型,它比从一开始就内置于 .NET 中的任何其他类型具有更多的精度位,并且其值都可以简明地以 base-10 格式表示。然而,它体积庞大且速度慢,并且因为它是浮点类型,它不再能够满足“精确”类型的典型公理(例如,对于任何 X 和 Y,(X+Y-Y)==X 应该返回 true 或抛出溢出异常)。我猜想它是浮点类型而不是定点类型,因为对小数点右侧的位数犹豫不决。实际上,使用 128 位定点格式可能会更快,而且同样有用,但 Decimal 类型就是这样。

顺便说一下,像 PL/I 这样的语言可以很好地处理定点类型,因为它们认识到精度是存储位置而不是的函数。不幸的是,.NET 没有提供任何好的方法,通过它可以将变量定义为持有 Fixed(6,3) 并自动缩放和移动存储在其中的 Fixed(5,2)。将精度作为值的一部分意味着将值存储到变量中将更改变量在小数点右侧表示的位数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-12-13
    • 1970-01-01
    • 1970-01-01
    • 2019-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多