x86 调试和发布版本之间的双精度值更改为 1 位

1 bit in double value changes between x86 Debug and Release build

提问人:Jens 提问时间:10/16/2018 最后编辑:Jens 更新时间:1/11/2023 访问量:205

问:

在一些数字代码中,我注意到针对 x86 或使用 AnyCPU+“首选 32 位”编译时的调试和发布版本给出了不同的结果。 我已将代码分解为重现该问题的几乎最低限度。 事实证明,在其中一个计算步骤中,只有 1 位发生了变化。

代码:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim tau = 0.000001
    Dim a = 0.5
    Dim value2 = (2 * Math.PI * 0.000000001 * tau) ^ a * Math.Sin(a * (Math.PI / 2))
    RichTextBox1.Text = BitConverter.DoubleToInt64Bits(value2).ToString
End Sub

在 Int64 中,Debug 版本和 Release 版本给出(注意最后一位数字)。44985588517386553404498558851738655341

我尝试在调试版本中手动启用优化(以及 DEBUG 常量和 pdb 生成),但结果保持不变。仅将完整生成类型更改为“发布”才会更改结果。

更进一步,我尝试比较 IL,正如 Telerik 的 JustDecompile 所给出的那样:

调试版本:

.method private instance void Button1_Click (
        object sender,
        class [mscorlib]System.EventArgs e
    ) cil managed 
{
    .locals init (
        [0] float64 V_0,
        [1] float64 V_1,
        [2] float64 V_2,
        [3] int64 V_3
    )

    IL_0000: nop
    IL_0001: ldc.r8 1E-06
    IL_000a: stloc.0
    IL_000b: ldc.r8 0.5
    IL_0014: stloc.1
    IL_0015: ldc.r8 6.2831853071795863E-09
    IL_001e: ldloc.0
    IL_001f: mul
    IL_0020: ldloc.1
    IL_0021: call float64 [mscorlib]System.Math::Pow(float64,  float64)
    IL_0026: ldloc.1
    IL_0027: ldc.r8 1.5707963267948966
    IL_0030: mul
    IL_0031: call float64 [mscorlib]System.Math::Sin(float64)
    IL_0036: mul
    IL_0037: stloc.2
    IL_0038: ldarg.0
    IL_0039: callvirt instance class [System.Windows.Forms]System.Windows.Forms.RichTextBox CETestGenerator.Form1::get_RichTextBox1()
    IL_003e: ldloc.2
    IL_003f: call int64 [mscorlib]System.BitConverter::DoubleToInt64Bits(float64)
    IL_0044: stloc.3
    IL_0045: ldloca.s V_3
    IL_0047: call instance string [mscorlib]System.Int64::ToString()
    IL_004c: callvirt instance void [System.Windows.Forms]System.Windows.Forms.RichTextBox::set_Text(string)
    IL_0051: nop
    IL_0052: ret
}

发布版本:

.method private instance void Button1_Click (
        object sender,
        class [mscorlib]System.EventArgs e
    ) cil managed 
{
    .locals init (
        [0] float64 V_0,
        [1] float64 V_1,
        [2] float64 V_2,
        [3] int64 V_3
    )

    IL_0000: ldc.r8 1E-06
    IL_0009: stloc.0
    IL_000a: ldc.r8 0.5
    IL_0013: stloc.1
    IL_0014: ldc.r8 6.2831853071795863E-09
    IL_001d: ldloc.0
    IL_001e: mul
    IL_001f: ldloc.1
    IL_0020: call float64 [mscorlib]System.Math::Pow(float64,  float64)
    IL_0025: ldloc.1
    IL_0026: ldc.r8 1.5707963267948966
    IL_002f: mul
    IL_0030: call float64 [mscorlib]System.Math::Sin(float64)
    IL_0035: mul
    IL_0036: stloc.2
    IL_0037: ldarg.0
    IL_0038: callvirt instance class [System.Windows.Forms]System.Windows.Forms.RichTextBox CETestGenerator.Form1::get_RichTextBox1()
    IL_003d: ldloc.2
    IL_003e: call int64 [mscorlib]System.BitConverter::DoubleToInt64Bits(float64)
    IL_0043: stloc.3
    IL_0044: ldloca.s V_3
    IL_0046: call instance string [mscorlib]System.Int64::ToString()
    IL_004b: callvirt instance void [System.Windows.Forms]System.Windows.Forms.RichTextBox::set_Text(string)
    IL_0050: ret
}

正如你所看到的,它几乎是一样的。唯一的区别是两个额外的 nop 命令(它们不应该做任何事情?

现在我的问题是,我做错了什么,是编译器还是框架在做一些奇怪的事情,或者就是这样? 我知道不是每个数字都可以精确地用双倍表示。这就是我比较 Int64 表示形式的原因。不过,我的理解是,结果不应该在构建之间改变。

鉴于仅启用优化并不能改变它,那么可能导致这种情况的 Debug 和 Release 之间还有什么区别?

我正在为 .NET Framework 4.5 进行编译,正如我上面所说,该错误仅发生在 x86 版本(或 AnyCPU+Prefer 32 位选项)中。

编辑:根据 Tomers 的评论,这个问题处理了类似的想法,同时更侧重于调试/发布构建的差异。我仍然觉得有点奇怪,当运行相同的代码时,不同的架构应该给出不同的结果。这是设计使然吗?我怎样才能相信我计算出的值呢?

.NET vb.net Roslyn

评论

1赞 41686d6564 10/16/2018
我可以在 VB 和 C# 中重现这一点。这是 C# 版本,以防您想使用它。
2赞 Tomer 10/16/2018
stackoverflow.com/questions/90751/ 的副本......
1赞 Jens 10/16/2018
感谢您验证 Ahmed,感谢 Tomer 的重复标记。由于调试/发布与 32/64 位之间的比较,它有些不同,但我认为原因至少是相似的。从未听说过 FPU 使用 80 位寄存器的事实;
2赞 usr 10/16/2018
这是您的要求还是您好奇?扩展精度是 .NET 上已知的东西。您可以通过存储到字段中来强制精确。另外,我听说一个简单的演员阵容就足够了。因此,将双精度值转换为双精度值。您需要验证这一点,因为 C# 编译器和 JIt 都可能破坏这一点。我相信对于兼容性,他们没有。
3赞 Hans Passant 10/16/2018
两个结果都是正确的,您显示的数字远远超过双精度可以存储的数字。如果您出于某种原因更喜欢一致性,那么您需要利用英特尔提供的修复程序。项目>属性>编译选项卡,取消选中“首选 32 位”。背景资料在这里

答:

0赞 schlebe 1/11/2023 #1

上,我已经测试了您的代码(进行了一些调整)使用和使用。Windows 11Visual Studio 2019.Net Framework 4.7.2Visual Studio 2022.Net 6

使用 或 版本显示的结果始终相同。我看不出有什么区别。DebugRelease

Visual Studio 2019始终返回一个以 40 结尾的值作为调试版本,并在为 cible 编译时返回一个以 41 结尾的值。Visual Studio 2022x64

Visual Studio 2022我的 PC 上安装的是一个 64 位应用程序。

我使用了以下程序

Imports System

Module Program
    Sub Main(args As String())
        Dim tau As Double = 0.000001
        Dim a As Double = 0.5
        Dim n1 = 2 * Math.PI * 0.000000001 * tau
        Dim n2 = Math.Sin(a * (Math.PI / 2))
        Dim n3 = n1 ^ a
        Dim n4 = n3 * n2
        Dim x2 = Math.Sin(Math.PI * a / 2)
        Dim x3 = a * Math.PI / 2
        Dim x4 = Math.Sin(x3)
        Dim x5 = Math.Sin(0.78539816339744828)

        Dim value2 As Double = (2 * Math.PI * 0.000000001 * tau) ^ a * Math.Sin(a * (Math.PI / 2))
        Dim s As String = BitConverter.DoubleToInt64Bits(value2).ToString

        Console.WriteLine(">> 64 bits signed integer : " & s)
        Console.WriteLine(">> n1 : " & n1)
        Console.WriteLine(">> n2 : " & n2)
        Console.WriteLine(">> n3 : " & n3)
        Console.WriteLine(">> n4 : " & n4)
        Console.WriteLine(">> 64 bits n4: " & BitConverter.DoubleToInt64Bits(n4).ToString)
        Console.WriteLine(">> x2 : " & x2)
        Console.WriteLine(">> x3 : " & x3)
        Console.WriteLine(">> x4 : " & x4)
        Console.WriteLine(">> x5 : " & x5)
    End Sub
End Module

x86构建显示以下结果

>> 64 bits signed integer : 4498558851738655340
>> n1 : 6,283185307179586E-15
>> n2 : 0,7071067811865475
>> n3 : 7,926654595212022E-08
>> n4 : 5,604991216397928E-08
>> 64 bits n4: 4498558851738655340
>> x2 : 0,7071067811865475
>> x3 : 0,7853981633974483
>> x4 : 0,7071067811865475
>> x5 : 0,7071067811865475

x64构建显示以下结果

>> 64 bits signed integer : 4498558851738655341
>> n1 : 6,283185307179586E-15
>> n2 : 0,7071067811865476
>> n3 : 7,926654595212022E-08
>> n4 : 5,6049912163979286E-08
>> 64 bits n4: 4498558851738655341
>> x2 : 0,7071067811865476
>> x3 : 0,7853981633974483
>> x4 : 0,7071067811865476
>> x5 : 0,7071067811865476

和 build 之间的区别是由于函数(参见变量 x4 和 x5)!x86x64Math.Sin()