7

当您阅读MSDN 时System.Single

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

和 C# 语言规范:

和类型使用 32 位单精度floatdouble64 位双精度 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));
  }
}

除了编写一些环境信息和编译配置外,程序只考虑两个floats(即ab)及其乘积。最后四行是有趣的。以下是使用Debug x86(左)、Release x86(中)和x64(右)编译后在 64 位机器上运行的输出:

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

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

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

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

0 . 1(001 0100 0111 1010 1110 0)(001 0100 0111 1010 1110 0)...

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

0 . 1(001 0100 0111 1010 1110 0)(001      (*)

我们已经四舍五入(在本例中向下舍入)到最接近的可表示的Single. 现在,数字“一百”(b)是:

110 0100 .       (**)

在二进制。计算数字的完整乘积(*)(**)给出:

 11 1001 . 1111 1111 1111 1111 1110 0100

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

 11 1010 . 0000 0000 0000 0000 00

我们四舍五入的地方是因为下一位是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)应该是等效的(根据 IEEE)时,看似无辜的操作(例如通过将一个拉出到一个临时局部变量)将一个表达式拆分为两个表达式会改变行为,这难道不是一个问题吗?程序员如何提前知道运行时是否选择保持float“人工”额外(64 位)精度?
  5. 为什么允许在发布模式下编译的“优化”改变算术?

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

4

1 回答 1

8

我没有检查过你的算术,但我之前肯定见过类似的结果。除了调试模式会有所不同,分配给局部变量和实例变量也会有所不同。根据 C# 4 规范的第 4.1.6 节,这是合法的:

浮点运算可以以比运算结果类型更高的精度执行。例如,一些硬件架构支持“扩展”或“长双精度”浮点类型,其范围和精度比该double类型更大,并使用这种更高精度类型隐式执行所有浮点运算。只有以过高的性能成本为代价,才能使这种硬件架构以较低的精度执行浮点运算。C# 不需要一个实现来牺牲性能和精度,而是允许对所有浮点运算使用精度更高的类型。除了提供更精确的结果之外,这很少有任何可衡量的影响。[...]

我不能确定这是否是这里发生的事情,但我不会感到惊讶。

于 2012-09-28T17:30:47.357 回答