除法 C 后双精度和小数的数值精度#

Numerical Precision of doubles and decimals after division C#

提问人:makigjuro 提问时间:5/14/2021 更新时间:5/14/2021 访问量:221

问:

我正在处理很多数学运算,主要是除法,在完成所有计算后,所有数字都与初始状态匹配,这一点非常重要。 例如,我正在 C# 中测试以下双精度代码:

        double total = 1000;
        double numberOfParts = 6;

        var sumOfResults = 0.0;
        for (var i = 0; i < numberOfParts; i++)
        {
            var result = total / numberOfParts;
            sumOfResults += result;
        }

此代码给出了 sumOfResults 的结果:999.9999999999999,我预计为 1000。 使用小数时也会发生类似的问题,只是更精确:

        decimal total = 1000m;
        decimal numberOfParts = 6m;

        var sumOfResults = decimal.Zero;
        for (var i = 0; i < numberOfParts; i++)
        {
            var result = total / numberOfParts;
            sumOfResults += result;
        }

预期的 sumOfResults 为 1000M,但发现 1000.00000000000000000000001M。因此,即使在这里,当我需要与1000的初始状态进行比较时,我也永远不会拥有与数字除法之前相同的状态。

我知道数值分析字段和所有内容,但是是否有一些库可以帮助我在所有除法结果的总和后获得 1000 的确切数字?

C# 精度 数值分析

评论

0赞 juharr 5/14/2021
你需要一些东西来存储值作为整数的分数(而且你不必对像 Pi 这样的无理数做任何事情)。但是,对于 Stack Overflow 来说,寻找库是无关紧要的。或者,您可以在精度内进行比较,而不是尝试进行精确匹配。 哪里是一些小数字。Match.Abs(actual - expected) < epsilonepsilon
1赞 Dmitry Bychenko 5/14/2021
要么使用理性类型 ( searchcode.com/codesearch/view/8725703) ,要么将 / 值与公差进行比较:say, BigRationaldoubledecimalbool equal = Math.Abs(expected - actual) <= tolerance;
0赞 makigjuro 5/14/2021
谢谢 BigRational 链接,这看起来不错。顺便说一句,我使用像 bool 等于 = Math.Abs(预期 - 实际) <= tolerance 这样的技术;但是这些公差在我所做的数百万次操作中积累起来,它们会产生问题。
0赞 JAlex 5/14/2021
请参阅最近有关 C# 精度的帖子
1赞 JAlex 5/14/2021
如果用双倍表示地球的周长,则精度等于 7 纳米。这还不够吗?或者是希望数字显示到最接近的整数的可见性问题?

答:

2赞 Dmitry Bychenko 5/14/2021 #1

如果你正在寻找确切的结果,你必须使用有理数;有很多实现类型的程序集;比如说,你可以尝试使用我自己的 HigherArithmetics(它适用于 .Net 5):BigRational

  using HigherArithmetics.Numerics;

  ... 

  BigRational total = 1000;
  BigRational numberOfParts = 6;

  BigRational sumOfResults = 0;
  
  for (var i = 0; i < numberOfParts; i++) {
    var result = total / numberOfParts;
    sumOfResults += result;
  }

  Console.Write(sumOfResults);

结果:

  1000

但是,如果要使用标准或类型,则必须与公差进行比较:doubledecimal

  double tolerance = 1e-6; 

  double total = 1000;
  double numberOfParts = 6;

  double sumOfResults = 0;
  
  for (var i = 0; i < numberOfParts; i++) {
    var result = total / numberOfParts;
    sumOfResults += result;
  }

  sumOfResults = Math.Abs(total, sumOfResults) <= tolerance
    ? total
    : sumOfResults;

  Console.Write(sumOfResults);

最后,还有一种可能性是四舍五入的答案:

  sumOfResults = Math.Round(sumOfResults, 6);

评论

0赞 makigjuro 5/14/2021
太好了,BigRational 就是我要找的。谢谢很多!
0赞 JAlex 5/14/2021
公差应为值的倍数,该值的倍数应使用 计算。1ulpxeps(x) = 2^(floor(log(x,2))-52)
0赞 Dmitry Bychenko 5/14/2021
@JAlex:没有必要;当我们将几个不精确的值相加时,总误差很可能大于eps
0赞 JAlex 5/14/2021
@DmitryBychenko - 我同意。我应该说几个倍数.根据现在的情况,位可能已经失去了精度。对于常规数学,误差的因数可能为数千(通常是 2 的幂),而对于三角函数,它接近数亿。1ulp1ulpu1lp
1赞 JAlex 5/14/2021 #2

这更像是人类的感知,而不是真正的数字问题。由于机器精度的原因,几乎每个浮点数都不准确。从数学上讲,1000.0 和 999.999999999999997 之间的差值对于大多数操作来说已经足够了。

这个解决方案对你来说可能看起来很奇怪,但它可以解决计算读数带来的不准确焦虑。

    double total = 1000;
    double numberOfParts = 6;

    var sumOfResults = 0.0;
    for (var i = 0; i < numberOfParts; i++)
    {
        var result = total / numberOfParts;
        sumOfResults += result;
    }

    Console.WriteLine((float)sumOfResults);
    // 1000

只需降低人类可读输出的精度 您看到提高精度如何使事情变得更糟,因此请反其道而行之。系统在一定程度上执行此操作,因为函数会舍入最低有效位。double.ToString()

或者,您可以使用“gXXX”格式说明符控制要显示的有效位数。

    Console.WriteLine($"{sumOfResults:G5}");
    // 1000

总之,您看到的“问题”在所有具有 IEEE 754 浮点类型的计算机中都很常见,并且大多数数字都未准确表示。

例如,下面的定义如下所示,如下所示Math.PIC#

环境 价值
π 3.141592653589793238462643383279502884...
.NET 5 3.141592653589793
Framework v4.8 3.141592653589791