编译时与运行时的 .NET 浮点舍入错误

本文关键字:舍入 错误 NET 运行时 编译 | 更新日期: 2023-09-27 18:32:02

我最近为测试用例设置了一些数据,检查浮点数据类型的舍入错误,并遇到了一些意想不到的结果。 我预计案例 t2 和 t3 会产生与 t1 相同的结果,但在我的机器上并非如此。 谁能告诉我为什么?

怀疑差异的原因是 t2 和 t3 在编译时被评估,但令我惊讶的是编译器完全忽略了我在评估期间强制它使用中间浮点数据类型的尝试。 c# 标准中是否有某些部分要求计算具有最大可用数据类型的常量,而不考虑指定的常量?

这是在运行 .net 4.5.2 的 win7 64 位英特尔机器上。

  float temp_t1 = 1/(3.0f);
  double t1 = (double)temp_t1;
  const float temp_t2 = 1/(3.0f);
  double t2 = (double)temp_t2;
  double t3 = (double)(float)(1/(3.0f));
  System.Console.WriteLine( t1 ); //prints 0.333333343267441
  System.Console.WriteLine( t2 ); //prints 0.333333333333333
  System.Console.WriteLine( t3 ); //prints 0.333333333333333

编译时与运行时的 .NET 浮点舍入错误

人们经常对浮点计算的一致性有疑问。在这一点上,.NET Framework 几乎没有给出任何保证。引用Eric Lippert的话:

C# 编译器、抖动和运行时都具有广泛的灵活性,可以随时随心所欲地为您提供比规范要求的更准确的结果 - 它们不需要始终如一地选择这样做,事实上它们不需要。

在这种特殊情况下,答案是直截了当的。发布版本的原始 IL:

IL_0000: ldc.r4 0.333333343
IL_0005: conv.r8
IL_0006: ldc.r8 0.33333333333333331
IL_000f: stloc.0
IL_0010: ldc.r8 0.33333333333333331
IL_0019: stloc.1
IL_001a: call void [mscorlib]System.Console::WriteLine(float64)
IL_001f: ldloc.0
IL_0020: call void [mscorlib]System.Console::WriteLine(float64)
IL_0025: ldloc.1
IL_0026: call void [mscorlib]System.Console::WriteLine(float64)
IL_002b: ret

这里的所有算术都是由编译器完成的。在 Roslyn 编译器中,temp_t1 是一个变量这一事实会导致编译器发出加载 4 字节浮点数的 IL,然后将其转换为双精度值。我相信这与以前的版本一致。在其他两种情况下,编译器以双精度执行所有算术并存储这些结果。第二种和第三种情况没有区别也就不足为奇了,因为编译器不会在 IL 中保留本地常量。

C# 浮点行为基于底层 CPU,使用 IEEE 754 格式。如果你想真正看到发生了什么,你需要通过将它们转换为字节来查看二进制格式的数字。当您打印它们时,它们从基数 2 转换为基数 10,并且您正在进行大量处理。

这就是我怀疑正在发生的事情。您的第一个计算 (temp_t1) 是使用单精度浮点数,尾数为 23 位。我怀疑,但没有确认,temp_t2 和 t2 已被编译器中的优化组件转换,因此temp_t2不是使用单精度浮点数计算的,而是双精度计算的,并且 t2 拾取了该值。

有关浮点行为的详细信息:https://msdn.microsoft.com/en-us/library/aa691146(v=vs.71).aspx