将计算出的双精度数转换为十进制可以校正精度误差吗?

时间:2017-09-01 07:42:03

标签: c# casting double decimal precision

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

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) );

以上代码将生成以下输出:

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? 我可以依靠这个技巧来避免双重计算的精确问题吗?

3 个答案:

答案 0 :(得分:2)

  

有人知道计算机在这种情况下如何知道在转换为十进制时它应该将1000.0000000000002值更改为1000吗?

首先是演员:

(decimal)IncorrectPPM

等同于构造函数调用see here on SO

new decimal(IncorrectPPM)

如果你阅读MSDN page about the decimal constructor,你会发现以下评论:

  

此构造函数使用舍入到最接近的值将值舍入为15位有效数字。即使数字超过15位且较低有效数字为零,也会这样做。

这意味着

1000.0000000000002 
               ^ ^  
            15th 17th significant digit

将四舍五入为:

1000.00000000000 
               ^ 
            15th significant digit
  

我可以依靠这个技巧来避免双重计算的精确问题吗?

不,在计算IncorrectPPMsee online at ideone时,您无法想象以下结果:

1000.000000000006
               ^  
            15th significant digit

将四舍五入为:

1000.00000000001
               ^  
            15th significant digit

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

  1. threshold添加一点epsilon,例如:

    double threshold = 1000.0001;
    
  2. 从{<1}}更改您的演员:

    IncorrectPPM

    为:

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

    使用Math.Round() function,但要小心/* 1000.000000000006 will be rounded to 1000.0000 */ double CorrectedPPM = Math.Round(IncorrectPPM, 4); 表示小数无效数字

答案 1 :(得分:1)

您的阈值太小或者您将结果四舍五入到一定的小数。小数点越多,评估越精确。

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

你可以尽可能准确。

答案 2 :(得分:0)

decimaldouble在精确度方面存在根本差异,并且根植于数字的存储方式:

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

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

实际上,这意味着当您比较浮点数时,您必须始终知道有多少位数是重要的。注意,这意味着总位数(即小数点前后的位数)。一旦您知道了精度(或选择一个适合您的精度并提供足够的误差范围),您就可以决定将值用于比较所需的位数。让我们根据您的数据说出六位十进制数的精度是合适的,您可以这样比较您的阈值:

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

请注意,您只是围绕比较,而不是围绕您的价值。

转换为decimal时您要做的是隐式地将decimal的精度应用于浮点数。这只不过是首选的舍入解决方案,只是对精度的控制较少,并且会产生额外的性能影响。所以不,转换为decimal并不可靠,特别是对于大数字。