我有一个应用程序,我累积十进制值(加法和减法)。我使用十进制类型而不是双精度,以避免累积错误。但是,我遇到的情况是行为不是我所期望的。
我有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();
}
在评论中有一些讨论为什么这些高精度是必要的,所以我想我在这里总结一下。出于显示目的,我当然围绕这些操作的结果,但我对所有内部表示使用十进制。一些计算在整个过程中采用分数,这导致数字超出了十进制类型的精度。
然而,我要注意尽量保持一切稳定以便积累。因此,例如,如果我将数量分成三分之三,我将x / 3,x / 3然后(x - x / 3 - x / 3)。这是一个系统,它考虑了经常被这样划分的物理量,所以我不想过早地通过舍入来引入偏差。例如,如果我将上面的x = 1舍入到小数点后三位,我将以0.333,0.333,0.334作为操作的三个部分。系统可以做的事情的精确度存在实际的物理限制,但是理想情况下,它应该做的事情的逻辑核算应该保持尽可能精确。主要的关键要求是系统的总量不应因这些不同的操作而改变。在上面的例子中,我发现十进制可以违反这个假设,所以我想更好地理解为什么会发生这种情况以及如何解决它。
答案 0 :(得分:1)
C#类型Decimal与COBOL中使用的十进制类型不同,COBOL实际上存储每个半字节一个十进制数字的数字,并使用类似于手动进行十进制数学的数学方法。相反,它是一种浮点类型,它简单地假设数量不会变得那么大,因此它使用较少的位用于指数,并使用剩余的128位而不是64位用于精确度。
但是作为浮点表示,即使非常简单的小数值也不能准确表示:例如,0.1需要二进制重复分数,并且可能不会存储为精确值。 (不是,对于double; Decimal可以不同地处理该特定值,但这通常是正确的。)
因此,仍然需要使用典型的浮点数学过程进行比较,其中通过仅将值接受到某一点来比较,添加,减去值等。由于大约有23个小数位的精度,例如选择16作为标准,并忽略最后的那些。
如需参考,请阅读What Every Computer Scientist Should Know About Floating Point Precision。
答案 1 :(得分:1)
Decimal
类型是一种浮点类型,它具有比从一开始就内置到.NET中的任何其他类型的精度更高的位数,并且其值在base-10中都可以简洁地表示格式。然而,它是庞大而缓慢的,并且因为它是浮点类型,它不再能够满足典型的“精确”类型的公理(例如,对于任何X和Y,(X+Y-Y)==X
应该返回true或抛出溢出异常)。我猜它是一个浮点类型而不是定点,因为关于应该在小数点右边的位数的犹豫不决。在实践中,拥有128位定点格式可能会更快,同样有用,但Decimal
类型就是它。
顺便提一下,像PL / I这样的语言适用于定点类型,因为它们认识到精度是存储位置而不是值的函数。不幸的是,.NET没有提供任何好的方法,通过它可以将变量定义为持有Fixed(6,3)
并自动缩放和移位存储在其中的Fixed(5,2)
。将精度作为值的一部分意味着将值存储到变量中会将变量所代表的位数更改为小数点的右侧。