在64位计算机上运行x86编译的代码时,单精度算术被破坏

Jep*_*sen 7 c# floating-point clr 64-bit single-precision

当您阅读MSDN时System.Single:

Single 符合IEC 60559:1989(IEEE 754)二进制浮点运算标准.

和C#语言规范:

这些floatdouble类型使用32位单精度和64位双精度IEEE 754格式表示[...]

然后:

根据IEEE 754算法的规则计算产品.

您很容易得到float类型及其乘法符合IEEE 754 的印象.

IEEE 754的一部分是多重定义.我的意思是,当你有两个float实例时,存在一个且只有一个float是他们的"正确"产品.不允许产品依赖于计算系统的某些"状态"或"设置".

现在,考虑以下简单程序:

using System;

static class Program
{
  static void Main()
  {
    Console.WriteLine("Environment");
    Console.WriteLine(Environment.Is64BitOperatingSystem);
    Console.WriteLine(Environment.Is64BitProcess);
    bool isDebug = false;
#if DEBUG
    isDebug = true;
#endif
    Console.WriteLine(isDebug);
    Console.WriteLine();

    float a, b, product, whole;

    Console.WriteLine("case .58");
    a = 0.58f;
    b = 100f;
    product = a * b;
    whole = 58f;
    Console.WriteLine(whole == product);
    Console.WriteLine((a * b) == product);
    Console.WriteLine((float)(a * b) == product);
    Console.WriteLine((int)(a * b));
  }
}
Run Code Online (Sandbox Code Playgroud)

通过写一些关于环境的信息并编译配置,该程序只考虑两个float(即ab)及其产品.最后四个写行是有趣的.这是使用Debug x86(左),Release x86(中)和x64(右)编译后在64位机器上运行它的输出:

调试x86(左),发布x86(中)和x64(右)

我们得出结论,简单float操作的结果取决于构建配置.

后面的第一行"case .58"是对两个floats 相等的简单检查.我们希望它独立于构建模式,但事实并非如此.接下来的两行我们希望是相同的,因为它不会改变任何东西来转换float为a float.但他们不是.我们也希望他们阅读,"True? True"因为我们正在将产品a*b与自身进行比较.我们希望输出的最后一行与构建配置无关,但事实并非如此.

为了弄清楚正确的产品是什么,我们手动计算.0.58(a)的二进制表示是:

0 . 1(001 0100 0111 1010 1110 0)(001 0100 0111 1010 1110 0)...
Run Code Online (Sandbox Code Playgroud)

括号中的块是永远重复的周期.此数字的单精度表示需要四舍五入为:

0 . 1(001 0100 0111 1010 1110 0)(001      (*)
Run Code Online (Sandbox Code Playgroud)

我们已经舍入(在这种情况下向下舍入)到最近的可表示Single.现在,数字"一百"(b)是:

110 0100 .       (**)
Run Code Online (Sandbox Code Playgroud)

在二进制.计算数字的完整产品(*)(**)给出:

 11 1001 . 1111 1111 1111 1111 1110 0100
Run Code Online (Sandbox Code Playgroud)

舍入(在这种情况下舍入)到单精度给出

 11 1010 . 0000 0000 0000 0000 00
Run Code Online (Sandbox Code Playgroud)

在那里我们四舍五入,因为下一位是1,而不是0(舍入到最近).因此我们得出结论,结果是58f根据IEEE.根据IEEE,这不是先验地给出,例如0.59f * 100f小于59f0.60f * 100f大于60f.

所以看起来代码的x64版本是正确的(上图中最右边的输出窗口).

注意:如果此问题的任何读者都有一个旧的32位CPU,那么听听上述程序的输出在他们的体系结构上会很有趣.

现在问题是:

  1. 上面是一个bug吗?
  2. 如果这不是一个错误,那么在C#规范中,它表示运行时可能会选择以float额外的精度执行乘法,然后"忘记"再次摆脱该精度?
  3. 如何将float表达式转换为类型float更改任何内容?
  4. 看似无辜的操作,例如将表达式分成两个表达式,例如将一个表达式转换(a*b)为一个临时局部变量,改变行为,当它们应该在数学上(根据IEEE)等效时,这不是一个问题吗?如果运行时选择保持float"人为"额外(64位)精度,程序员如何才能提前知道?
  5. 为什么在发布模式下编译的"优化"允许更改算术?

(这是在.NET Framework的4.0版本中完成的.)

Jon*_*eet 8

我没有检查过你的算术,但我以前看到过类似的结果.除了调试模式有所不同之外,分配局部变量和实例变量也会产生影响.根据C#4规范的4.1.6节,这是合法的:

可以以比操作的结果类型更高的精度执行浮点运算.例如,某些硬件体系结构支持"扩展"或"长双"浮点类型,其范围和精度高于double类型,并使用此更高精度类型隐式执行所有浮点运算.只有在性能成本过高的情况下,才能使这种硬件架构以较低的精度执行浮点运算.C#允许更高精度的类型用于所有浮点运算,而不是要求实现放弃性能和精度.除了提供更精确的结果外,这几乎没有任何可衡量的影响.[...]

我不能肯定地说这是不是在这里发生了什么,但我不会感到惊讶.

  • 这正是这里发生的事情.因为它符合语言规范,所以它不是一个bug,虽然它有点不幸(人们可能会试图说它是语言规范中的一个错误). (2认同)