C# IEEE754舍入

C# IEEE754 Rounding

提问人:pmcilreavy 提问时间:10/8/2023 最后编辑:pmcilreavy 更新时间:10/9/2023 访问量:105

问:

请考虑以下 C# 代码...

double x = Math.Round(72.6d, 2, MidpointRounding.ToZero);
double y = Math.Round(82.6d, 2, MidpointRounding.ToZero);

x成为并成为.72.59y82.6

但是为什么?通过这个IEEE754转换器,两者的小数部分是相同的。那么他们为什么不给出相同的结果呢?

我可以通过执行以下操作来解决问题。但我更感兴趣的是知道为什么它似乎没有按预期工作。(double)Math.Round(Convert.ToDecimal(72.6d), 2, MidpointRounding.ToZero)

C# 精度 IEEE-754

评论

0赞 Etienne de Martel 10/8/2023
如果你使用 .MidpointRounding.ToEven
0赞 pmcilreavy 10/8/2023
@EtiennedeMartel我想保留 2 dp 并在不四舍五入的情况下截断其余部分。我想,这应该是应该的。例如,如果我有我想要.如果我使用它,那将给出这不是我想要的。ToZero9.8589.85ToEven9.86
0赞 topsail 10/8/2023
好奇心越来越强
0赞 Eric Postpischil 10/9/2023
根据文档,是 .NET Core 3.0 中的新增功能。您是否可能使用无法识别的旧库运行它?你得到同样的结果吗?MidpointRounding.ToZeroMidpointRounding.ToZeroMidpointRounding.ToEven

答:

0赞 BateTech 10/8/2023 #1

TLDR:浮点类型,在进行数学运算时有精度损失; 四舍五入时乘以 10^digit,这会导致精度损失; 不舍入,仅截断值。因此,当四舍五入到小数点后 2 位时,该方法将计算(由于乘法时精度的双倍损失),然后被截断为,因为您正在使用,然后除以 100 返回。SingleDoubleRoundMidpointRounding.ToZeroRound72.6d*100=7259.9999999999991;7259MidpointRounding.ToZero72.59

详:它必须具有浮点数的精度和舍入期间的二进制表示形式与提供的文字值不同。正如您已经发现的那样,您可以改用十进制,这没有相同的舍入问题。

https://learn.microsoft.com/en-us/dotnet/api/system.math.round?view=netframework-4.7.2#rounding-and-precision

以下是文档中的原因部分:

为了确定舍入运算是否涉及中点 值,则 Round 方法将要舍入的原始值乘以 10n,其中 n 是返回中所需的小数位数 值,然后确定剩余的小数部分是否 该值大于或等于 .5。这是一个细微的变化 在平等测试中,正如“平等测试”中所讨论的 双重引用主题的部分,测试是否相等 由于浮点,浮点值存在问题 格式在二进制表示和精度方面的问题。这意味着 略小于 .5 的数字的任何小数部分 (由于精度损失)不会向上舍入。

以下是可能导致它的原因:

四舍五入中点值的精度问题最有可能 出现以下情况:

当小数值不能精确地表示时 浮点类型的二进制格式。

当要舍入的值由一个或多个计算得出时 浮点运算。

当要舍入的值是 Single 而不是 Double 时,或者 十进制。有关详细信息,请参阅下一节舍入和 单精度浮点值。

编辑:这是 72.59 的运行时值示例。在舍入过程中,它被乘以 100,然后用 截断。使用 ToZero。由于 72.59 * 100 存在浮点舍入精度问题,因此它最终为 7259.99999999999991,然后被截断为 7259 并在最后再次除以 100 得到 72.59。

我检查了Math.Round的源代码。以下是 Double MidpointRounding.ToZero 舍入的相关部分,注释中包含值。

value = 72.6; //value passed in
double num = roundPower10Double[digits]; //10^2 = 100
value *= num; //72.6d*100=7259.9999999999991
case MidpointRounding.ToZero:
    value = Truncate(value); //7259
value /= num; //7259 / 100 = 72.59;

现在 82.6 也是如此。当 * 100 时,它没有相同的精度问题。

value = 82.6; //value passed in
double num = roundPower10Double[digits]; //10^2 = 100
value *= num; //82.6d*100=8260
case MidpointRounding.ToZero:
    value = Truncate(value); //8260
value /= num; //8260 / 100 = 82.6;

示例代码显示:

static void Main(string[] args)
{
    double xl = 72.6d;
    double yl = 82.6d;

    Console.WriteLine("Values * 100d:");
    Console.WriteLine(xl * 100d);
    Console.WriteLine(yl * 100d);

    Console.WriteLine("Math.Round:");
    double x = Math.Round(xl, 2, MidpointRounding.ToZero);
    double y = Math.Round(yl, 2, MidpointRounding.ToZero);
    Console.WriteLine(x);
    Console.WriteLine(y);
}

调试输出:

debug output

另请查看我提供的链接中的“Round(Double, Int32, MidpointRounding) / Note to Callers”部分,该部分提供了类似的情况,其中 2.135 有问题,但 3.135 没有:

调用方说明 由于以下原因可能导致精度损失 将十进制值表示为浮点数或执行 对浮点值进行算术运算,在某些情况下 Round(Double, Int32, MidpointRounding) 方法可能不会显示为舍入 mode 参数指定的中点值。这是 在以下示例中进行了说明,其中 2.135 四舍五入为 2.13 而不是 2.14。发生这种情况是因为该方法在内部相乘 值乘以 10位,在本例中为乘法运算 遭受精度损失。

编辑2: 此外,无论中点如何,MidpointRounding.ToZero 将始终截断结果。请参阅此处的示例:https://learn.microsoft.com/en-us/dotnet/api/system.midpointrounding?view=net-7.0#examples

result = Math.Round(3.47m, 1, MidpointRounding.ToZero);
//result = 3.4

这意味着当我们得到 72.6 * 100 = 7259.999999999999991 时,由于算术运算期间的精度损失,那么由于您使用的是 MidpointRounding.ToZero,因此小数 .9 不会四舍五入到 7260,而是被截断并保留为 7259,然后返回 72.59 而不是 72.6。

评论

0赞 topsail 10/8/2023
原始问题的示例(72.59 和 82.59)中是否涉及任何中点?
0赞 pmcilreavy 10/8/2023
@batetech 请看我在问题中的链接。当您在该网站中输入 82.6 和 72.6 时,它会显示基于 IEEE-754 标准(在 C# 中实现)的浮点表示形式。如您所见,两者的小数部分是相同的。那么他们为什么不给出相同的结果呢?
1赞 Eric Postpischil 10/9/2023
这个答案不足以解释这个问题。当 72.6 和 82.6 转换为 时,结果正好是 72.5999999999999994315658113919198513031005859375 和 82.5999999999999994315658113919198513031005859375。链接的文档说该方法乘以 10^n,在本例中为 100。使用 IEEE-754“双精度”算术进行乘法运算会产生 7259.999999999999090905052982270717620849609375 和 8260。文档说“然后确定该值的剩余小数部分是否大于或等于 .5。...DoubleRoundRound
1赞 Eric Postpischil 10/9/2023
...对于 72.6,7259.9999999999990905052982270717620849609375 的分数部分明显大于 .5,因此应四舍五入为 7260,对于 82.6,8260 的分数部分明显小于 .5,因此应向下舍入为 8260。在中点 .5 附近没有明显的浮点误差产生任何问题。因此,还有其他一些问题,也许是文档中没有描述的其他一些算术。
1赞 BateTech 10/9/2023
根据 MidpointRounding 枚举文档和 Math.Round 源代码,ToZero 以这种方式工作似乎是(有问题的)设计。ToZero 是在 .NET Core 3.0 中添加的,该版本到 .NET 8 的文档中的示例使用非中点 3.47,并显示四舍五入到 1 dp 时向下舍入到 3.4。我同意它的工作原理令人困惑,尤其是与 AwayFromZero 相比。OP 在评论中说,不四舍五入的截断也是预期的。但在这种情况下,Double * 100 运算的精度损失是导致意外结果的原因。
5赞 Eric Postpischil 10/9/2023 #2

根据 Microsoft 文档,不执行“中点舍入”。Microsoft似乎在这里搞砸了界面:MidpointRounding.ToZero

  • MidpointRounding最初是作为枚举创建的,用于在四舍五入到最接近且平局与平局远离零 () 之间进行选择。ToEvenAwayFromZero
  • 后来又增加了额外的舍入方法:向负无穷大舍入()、向正无穷大舍入()和向零()舍入。请注意,这些不会四舍五入到最接近的方向,而是朝向其中一个方向;他们向其中一个方向四舍五入。例如,使用 、 3.1、3.2、3.5、3.6 和 3.8 都四舍五入为 3。Math.RoundToNegativeInfinityToPositiveInfinityToZeroToZero
  • Microsoft 没有使用新名称(如 )创建新枚举,而是将这些新值添加为 中的成员。RoundingMethodMidpointRounding

结果是,所有五种舍入方法都具有表单名称,尽管其中只有两种是与中点相关的舍入到最接近的方法。MidpointRounding.Name

(我们还看到使用术语不敏感,而不是 3.4 四舍五入到 3,即接近零。它没有四舍五入到零。IEEE 754 使用“toward”表示这些舍入方法。ToToward

除此之外,本文档告诉我们通过乘以 10n 并四舍五入到整数来操作,而不是正确确定原始数字的四舍五入,我们可以看到会发生什么:Math.Round

  • 72.6 转换为 ,产生最接近的可表示值 72.5999999999999994315658113919198513031005859375。乘以 100,得到 7259.9999999999990905052982270717620849609375。在朝向零的方向上四舍五入为整数,得到 7259。除以 100,生成 72.5900000000000003410605131648480892181396484375,如果使用的位数少于 17 位,则打印为 72.59。double
  • 82.6 转换为 ,生成 82.599999999999994315658113919198513031005859375。乘以 100,得到 8260。这种情况与前一种情况之间的区别是由于实数相对于可表示数字的位置的偶然性。然后将 8260 除以 100,生成 82.599999999999994315658113919198513031005859375,如果使用的位数少于 16 位,则打印为 82.60。double