首页 文章

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

提问于
浏览
2

我正在开发一个计算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 .

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

3 回答

  • 1

    有人知道计算机在这种情况下如何知道在转换为十进制时它应该将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
    

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

    • 为你的 threshold 添加一个小ε,例如:
    double threshold = 1000.0001;
    
    • 从以下位置更改 IncorrectPPM 的演员表:
    double CorrectedPPM = (double)((decimal)IncorrectPPM);
    

    至:

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

    使用Math.Round() function,但要小心 Math.Round() 表示小数无效数字

  • 0

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

    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

    decimaldouble 在精度方面存在根本区别,其根源在于数字的存储方式:

    • decimal 是一个定点数,这意味着它提供固定数量的小数 . 使用定点数进行计算时,需要经常执行舍入操作 . 例如,如果将数字100.0006除以10,则必须将该值舍入为10.0001,假设您的小数点固定为四位小数 . 这种舍入误差在经济计算中通常很好,因为你有足够的小数可用,并且该点固定为小数位(即基数为10) .

    • double 是浮点数,以不同方式存储 . 它有一个符号(定义数字是正数还是负数),尾数和指数 . 尾数是0到1之间的数字,带有一定数量的有效数字(这就是我们所说的实际精度) . 指数定义小数点在尾数中的位置 . 因此小数点在尾数中移动(因此浮点) . 浮点数非常适合计算测量值和进行科学计算,因为精度通过计算保持不变,并且可以最小化舍入误差 .

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

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

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

    请注意,您只对比较进行舍入,而不是对值进行舍入 .

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

相关问题