在编译64位时导致FP精度严重下降的原因是什么?

平台:C#使用Visual Studio 2013。

我有一个运行在64位Haswell CPU上的Windows应用程序,它正常工作,启用了“Prefer 32-bit”。 我决定通过取消选择’Prefer 32-bit’来升级到’Prefer 64-bit’,并且应用程序的算法突然变为不正确的值。 我失去了算术精度的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位’总是理解如何正确组合不同精度的值。

错误在哪里:

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

我将此升级到Microsoft,最后通过JIT编译器团队中的某个人。 事实certificate这是有意的行为,即如果使用具有隐式类型转换的双精度浮点算法,则有可能必须修改您的C#代码。 到目前为止,我认为算术精度仅依赖于变量的长度和任何显式/隐式转换(当然,在IEEE定义的浮点计算规则内)。 此外,我认为将工作32位应用程序编译为64位的选择不会改变应用程序行为。

我感谢微软给我发送以下回复……

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

 lng + flt 

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

 ((float)lng + flt) 

该表达式告诉JIT编译器它应该为单精度浮点ADD操作生成代码。 因此,给定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。

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

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