关于c#:将计算出的双数转换为十进制是否会修正精度误差?

Would casting a calculated double number into decimal correct the precision error?

我正在开发一个计算PPM并检查它是否大于某个阈值的应用程序。我最近发现了浮点计算的精度误差。

1
2
3
4
5
6
7
8
9
10
11
12
double threshold = 1000.0;
double Mass = 0.000814;
double PartMass = 0.814;
double IncorrectPPM = Mass/PartMass * 1000000;
double CorrectedPPM = (double)((decimal)IncorrectPPM);

Console.WriteLine("Mass = {0:R}", Mass);
Console.WriteLine("PartMass = {0:R}", PartMass);
Console.WriteLine("IncorrectPPM = {0:R}", IncorrectPPM);
Console.WriteLine("CorrectedPPM = {0:R}", CorrectedPPM);
Console.WriteLine("Is IncorrectPPM over threshold?" + (IncorrectPPM > threshold) );
Console.WriteLine("Is CorrectedPPM over threshold?" + (CorrectedPPM > threshold) );

上述代码将产生以下输出:

1
2
3
4
5
6
Mass = 0.000814
PartMass = 0.814
IncorrectPPM = 1000.0000000000002
CorrectedPPM = 1000
Is IncorrectPPM over threshold? True
Is CorrectedPPM over threshold? False

如您所见,计算的ppm 1000.0000000000002有一个尾随的2,这导致我的应用程序错误地判断该值超过1000阈值。计算的所有输入都以双精度值的形式提供给我,所以我不能使用十进制计算。此外,由于计算值可能导致阈值比较不正确,所以无法对其进行舍入。

我注意到,如果我将计算出的双精度数转换成十进制,然后再将它转换回双精度数,那么1000.0000000000002号就被更正为1000

问题:有人知道在这种情况下,计算机如何知道当转换为十进制时,它应该将1000.0000000000002值更改为1000?我可以依靠这个技巧来避免重复计算的精度问题吗?


Does anyone know how the computer know in this case that it should change the 1000.0000000000002 value to 1000 when casting to decimal?

首先是演员:

1
(decimal)IncorrectPPM

相当于构造函数调用,请参见以下内容:

1
new decimal(IncorrectPPM)

如果您在msdn页面上阅读有关decimal构造函数的内容,您会发现以下注释:

This constructor rounds value to 15 significant digits using rounding to nearest. This is done even if the number has more than 15 digits and the less significant digits are zero.

那意味着

1
2
3
1000.0000000000002
               ^ ^  
            15th 17th significant digit

将四舍五入为:

1
2
3
1000.00000000000
               ^
            15th significant digit

Can I rely on this trick to avoid the precision issue of double calculation?

不,在计算IncorrectPPM时,您无法想象以下结果,请参见IDeone的在线部分:

1
2
3
1000.000000000006
               ^  
            15th significant digit

将四舍五入为:

1
2
3
1000.00000000001
               ^  
            15th significant digit

要解决与阈值比较的问题,通常有两种可能性。

  • 在你的threshold上加一点epsilon,例如:

    1
    double threshold = 1000.0001;
  • 将你的IncorrectPPM的演员阵容从:

    1
    double CorrectedPPM = (double)((decimal)IncorrectPPM);

    到:

    1
    2
    /* 1000.000000000006 will be rounded to 1000.0000 */
    double CorrectedPPM = Math.Round(IncorrectPPM, 4);

    使用Math.Round()函数,但要小心,Math.Round()表示小数,不是有效数字。


  • 要么你的阈值太小,要么你把结果四舍五入到一定数量的小数。小数越多,评估就越精确。

    1
    2
    3
    4
    5
    double threshold = 1000.0;
    double Mass = 0.000814;
    double PartMass = 0.814;
    double IncorrectPPM = Mass/PartMass * 1000000;
    double CorrectedPPM = Math.Round(IncorrectPPM,4); // 1000.0000 will output 1000

    你可以随心所欲地做到精确。


    decimaldouble在精度方面存在根本性差异,其根源在于数字的存储方式:

    • decimal是一个固定的点号,这意味着它提供了一个固定的小数位数。使用固定点号进行计算时,需要非常频繁地执行舍入操作。例如,如果将数字100.0006除以10,则必须将该值四舍五入到10.0001,因为小数点固定为四位小数。这种舍入误差在经济计算中通常是很好的,因为您有足够的小数,并且该点固定为小数点(即以10为底)。
    • double是一个浮点数,存储方式不同。它有一个符号(定义数字是正数还是负数)、一个尾数和一个解释词。尾数是一个介于0和1之间的数字,带有一定数量的有效数字(即我们所说的实际精度)。指数定义小数点在尾数中的位置。所以小数点在尾数中移动(因此是浮点)。浮点数对于计算测量值和进行科学计算非常有用,因为通过计算精度保持不变,并且可以最小化舍入误差。

    您所面临的基本问题是,您只能依赖其精度范围内的浮点数的值。这包括尾数的实际精度和通过计算累积的舍入误差。另外,由于尾数是以二进制格式存储的,当您将尾数与1000进行比较时,会将其转换为十进制数,因此通过这种转换,您还有一个额外的不精确性。通常在固定的点号中不会出现这个问题,因为有效的十进制数字是明确定义的(并且在计算过程中接受舍入误差)。

    在实践中,这意味着当您比较浮点数时,您必须始终知道有多少位数是重要的。请注意,这是指总位数(即小数点前后的位数)。一旦知道了精度(或选择一个适合您的精度并提供足够的误差范围),您就可以决定需要多少位数字来舍入您的值以进行比较。假设根据您的数据,六位十进制数字的精度是适当的,您可以这样与阈值进行比较:

    1
    bool isWithinThreshold = Math.Round(PPM, 6) > 1000D;

    请注意,您只进行舍入比较,而不舍入值。

    在转换到decimal时,您要做的是隐式地将decimal的精度应用于浮点数。这只不过是舍入的首选解决方案,对精度的控制更少,对性能的影响也更大。因此,不,向decimal的转换是不可靠的,尤其是对于大量的转换。