【问题标题】:What causes significant loss of FP precision when compiling for 64-bit?编译 64 位时导致 FP 精度显着下降的原因是什么?
【发布时间】:2014-11-07 21:44:37
【问题描述】:

平台:使用 Visual Studio 2013 的 C#。

我有一个在 64 位 Haswell CPU 上运行的 Windows 应用程序,它在启用“首选 32 位”的情况下正常工作。我决定通过取消选择“首选 32 位”来升级到“首选 64 位”,应用程序的算法突然变为不正确的值。 我丢失了 29 位算术精度(这是我对双精度浮点尾数和单精度浮点尾数大小差异的估计)。这里的算术精度差异很大!

C# 代码……测试用例:

using System;
class lngfltdbl
{
    static void Main()
    {
        long   lng = 2026872;
        float  flt = 0.3F;
        double dbl = lng + flt;
        Console.WriteLine(dbl);
    }
}

预期结果(在选择“首选 32 位”时看到):

dbl == 2026872.30000001
(PERFECT! CORRECT to 14 decimal places)

获得的结果(在取消选择“首选 32 位”时看到):

dbl == 2026872.25
(ERROR!  CORRECT to 7 DECIMAL PLACES ONLY!)

请注意:过去我对隐式转换很满意,因为“首选 32 位”始终了解如何正确组合不同精度的值。

【问题讨论】:

  • 我想指出您的 lng + flt 表达式是自找麻烦 - 您不应该在没有明确转换的情况下混合类型(正如 Microsoft 回答所暗示的那样)。我不认为所有大写字母都值得小心。
  • 请将其改写为问答。你可以回答你自己的问题!这是一个非常有趣且相当奇怪的观察结果,我认为它会对网站做出重大贡献。
  • @GregRos 我觉得问题应该是“编译器选项可以有功能效果吗?”答案是“是的,很明显。事实上,对于没有功能效果的编译器来说,这是例外而不是规则。例如,GCC 的-mfpmath=387 [GCC 中与问题中讨论的选项最接近的等效项] 改变了生成代码的语义”。如果这在 C# 中没有正确记录,它应该是针对 C# 文档的错误报告,如果是,这只是一个“duh”评论,而不是一个很大的贡献。
  • 我正在按照建议进行更改,但无法添加我从 Microsoft 的 JIT 编译器团队收到的答案。 (我最初不想分开答案,因为我不打算为此获得任何功劳)

标签: c# visual-studio-2013 floating-point


【解决方案1】:

错误所在:

在专家协助下,我们观察到取消选择“首选 32 位”生成的汇编代码确实使用单精度指令 (cvtsi2ss; subss) 进行计算,然后将结果转换为双精度 (cvtss2sd : Convert Scalar双精度 FP 值转换为标量双精度 FP 值),最后将结果存储在双精度变量 (movsd) 中。这与检测到的错误的症状完全匹配,并解释了 29 位算术精度的损失。

我将此问题上报给了 Microsoft,并最终联系到了 JIT 编译器团队中的某个人。事实证明这是故意的行为,即如果使用带有隐式类型转换的双精度浮点算术,您可能必须修改您的 C# 代码。到目前为止,我认为算术精度仅取决于变量的长度和任何显式/隐式转换(当然,在 IEEE 定义的浮点计算规则内)。此外,我相信选择将工作的 32 位应用程序编译为 64 位不会改变应用程序的行为。

感谢 Microsoft 向我发送了以下回复……

您看到的行为是您提供的特定测试用例的预期行为。这里的关键是表达式

lng + flt

C# 编译器生成 IL 来评估这个表达式。它不考虑您将此表达式分配给什么。您的表达式和赋值依赖于插入到表达式中的隐式转换。 C# 编译器具有指定在为表达式生成 IL 时如何将隐式转换添加到表达式中的规则。在这种情况下,C# 编译器会添加这样的隐式转换:

((float)lng + flt)

这个表达式告诉 JIT 编译器它应该为单精度浮点加法运算生成代码。因此,考虑到 JIT 编译器提供的 IL,64 位目标生成的代码是完全合适的。它被告知(由 IL)计算 32 位大小的浮点结果,这就是您观察到的结果。

这里是这个方法的 IL:

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       26 (0x1a)
  .maxstack  2
  .locals init (int64 V_0,
           float32 V_1,
           float64 V_2)
  IL_0000:  ldc.i4     0x1eed78
  IL_0005:  conv.i8
  IL_0006:  stloc.0
  IL_0007:  ldc.r4     0.30000001
  IL_000c:  stloc.1
  IL_000d:  ldloc.0
  IL_000e:  conv.r4    ;; Force the conversion of ‘lng’ into a 32-bit float ‘r4’
  IL_000f:  ldloc.1
  IL_0010:  add
  IL_0011:  conv.r8
  IL_0012:  stloc.2
  IL_0013:  ldloc.2
  IL_0014:  call void [mscorlib]System.Console::WriteLine(float64)
  IL_0019:  ret
} // end of method lngfltdbl::Main

那么问题就变成了为什么 32 位目标 JIT 会产生不同(更精确)的结果?

这里的答案是,较旧的 32 位使用较旧的 x87 样式指令,并且我们一直声明 JIT 编译器可以以更高的精度为表达式计算中间浮点值。 32 位 JIT 编译器实际上会以更高的精度计算 32 位浮点表达式。这样做是因为这是使用较旧的 x87 样式指令时可用指令的自然行为。我们这样做是因为使用 x87 样式指令执行 32 位浮点运算会带来相当大的性能损失。我们记录了如果您需要 32 位浮点结果进行中间计算,您可以添加显式强制转换,并且当 JIT 看到显式强制转换时,需要将精度更改为 32 位浮点数。

对于您的情况,您需要在 ADD 指令的两个操作数中的任何一个上添加显式强制转换为“double”,以便 C# 编译器生成添加两个 64 位浮点数的 IL。 em>

这些源表达式中的任何一个都会计算出您想要的结果:

((double)lng + flt)
(lng + (double)flt)

【讨论】:

    猜你喜欢
    • 2010-09-08
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多