【问题标题】:Single-precision arithmetic broken when running x86-compiled code on a 64-bit machine在 64 位机器上运行 x86 编译代码时单精度算术中断
【发布时间】:2012-09-28 17:22:38
【问题描述】:

当你读到MSDN on 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));
  }
}

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

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

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

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

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(四舍五入到最近),所以我们向上取整。所以我们得出结论,根据 IEEE,结果是58f。根据 IEEE 的说法,这在任何情况下都没有先验,例如0.59f * 100f 小于59f,而0.60f * 100f 大于60f

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

注意:如果这个问题的任何读者有一个旧的 32 位 CPU,听听上面程序的输出在他们的架构上是什么会很有趣。

现在是问题:

  1. 以上是bug吗?
  2. 如果这不是错误,C# Specifcation 中的哪个位置说运行时可以选择以额外精度执行 float 乘法,然后“忘记”再次摆脱该精度?
  3. 如何将float 表达式转换为float 类型有什么改变?
  4. 看似无害的操作,例如将一个表达式拆分为两个表达式,这不是一个问题吗?将(a*b) 提取到临时局部变量会改变行为,当它们应该在数学上(根据 IEEE)等效时?程序员如何提前知道运行时是否选择以“人工”额外(64 位)精度保存float
  5. 为什么允许在发布模式下编译的“优化”来更改算法?

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

【问题讨论】:

  • 这就是为什么我们永远不应该检查浮点相等性;p
  • @leppie 好吧,检查“小于”或“大于”(即使在添加或减去一些 epsilon 之后)也会以完全类似的方式产生不可预测的结果。因此,当您的程序在某处使用浮点变量时,您永远无法知道程序是否始终如一地执行相同操作,因为 CLR 中的浮点运算是不可预测的?
  • 我有点讽刺。 :)

标签: c# floating-point 64-bit clr single-precision


【解决方案1】:

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

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

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

【讨论】:

  • 这正是这里发生的事情。因为它符合语言规范,所以它不是一个错误,尽管它有点不幸(有人可能会认为这是语言规范中的一个错误)。
  • @StephenCanon 我同意。上面的引用(谢谢,乔恩,这正是我所要求的,第 2 项。)有点说:“好吧,我们毕竟不符合 IEEE 标准;我们使用硬件的精度。我们知道它不等于IEEE,但我们仍然这样做。”
  • 乔恩,你是否也知道“如何将float 表达式转换为float 类型改变任何东西?”的答案?大多数程序员(和开发工具)会认为 cast 是空操作并将其删除。此外,人们可能会认为编译器忽略了它。但看起来这是一些“秘密”功能。或者这也是 C# 语言规范的一部分?
  • @JeppeStigNielsen:抱歉,才看到这个。不知道铸造位。我想我可以举一个例子,只是表明要进一步调查。
  • @supercat:同意。 Java 有 -strictfp (或任何所谓的标志;我现在忘记了),它可能会或可能不会给出 exactly 什么是可取的。至少有一些东西真是太好了:)
猜你喜欢
  • 2016-01-02
  • 2015-04-02
  • 2023-03-22
  • 2015-06-20
  • 1970-01-01
  • 1970-01-01
  • 2021-09-27
  • 1970-01-01
  • 2011-02-14
相关资源
最近更新 更多