当编译64位时,是什么导致FP精度的显著损失
本文关键字:精度 损失 FP 64位时 编译 是什么 | 更新日期: 2023-09-27 18:29:47
使用Visual Studio 2013的平台:C#。
我有一个Windows应用程序在64位Haswell CPU上运行,它在启用"首选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位"总是了解如何正确组合不同精度的值。
错误所在:
在专家的协助下,我们观察到,在取消选择"首选32位"的情况下生成的汇编代码确实使用了单精度指令(cvtsi2ss;subss)进行计算,然后将结果转换为双精度(cvtss2sd:将标量双精度浮点值转换为标量双精度FP值),最后将结果存储在双精度变量(movsd)中。这与检测到的错误的症状完全匹配,并解释了29位算术精度的损失。
我将此问题上报给了微软,并最终与JIT编译器团队中的某个人取得了联系。事实证明,这是有意为之的行为,即如果使用隐式类型转换的双精度浮点运算,则很可能您必须修改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)