来自相同 "if" 控件的不同结果

Different results from the same "if" control

当我意识到程序没有按预期运行时,我正在检查一些变量。 我知道问题是由于“if”块引起的。我正在使用 .Net Framework 4.7.2

这是一个实际的例子:

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

    Dim a As Single = 20
    Dim b As Single = 0.01

    Console.WriteLine("First test")
    If a / b > 2000 Then 'Same behavior with (a/b)>2000
        Console.WriteLine(String.Format("{0} > 2000", a / b))
    Else
        Console.WriteLine(String.Format("{0} <= 2000", a / b))
    End If

    Console.WriteLine("Second test")
    Dim c As Single = a / b
    If c > 2000 Then
        Console.WriteLine(String.Format("{0} > 2000", c))
    Else
        Console.WriteLine(String.Format("{0} <= 2000", c))
    End If

End Sub

这是输出:

First test
2000 > 2000
Second test
2000 <= 2000

如果我写

也会得到正确的结果
If CSng(a / b) > 2000 Then ...

为什么我需要这个铸件? 为什么我会得到两个不同的结果?

即使在源代码级别,一切都是 Single(至少,一旦您将常量设为 2000.0F 而不是 2000),我很确定问题出在这是在 FPU 内部以双精度完成的。代码序列几乎相同,但区别很关键:

If a / b > 2000.0F Then
fld     dword ptr [a]
fdiv    dword ptr [b]
fld     dword ptr ds:[const 2000]
fcomip  st, st(1)

(为了可读性换出实际地址;本地地址从堆栈中取出,文字从数据段中取出。)

比较:

c = a / b
If c > 2000.0F Then
fld     dword ptr [a]
fdiv    dword ptr [b]
fstp    dword ptr [c]
fld     dword ptr [c]
fld     dword ptr ds:[const 2000]
fcomip  st, st(1)

注意那里的内存往返。 x87 浮点数的一个有趣特性是(可能取决于您设置的舍入模式)它只会在存储值时舍入到定义的精度。在那之前,它将在内部以完全精确的方式工作。我认为这就是这里发生的事情:第一个序列没有中间存储,因此它以扩展精度发生,而第二个序列在 c 写入内存时强制舍入。

它变得更加有趣...这是在调试版本中,它保持第二步的确切代码序列完整无缺,以便与原始源代码完全对应以帮助调试。我认为经过优化的发布版本很有可能以第一个序列结束,并且你会得到相同的结果。

附录:我证实了我的假设,在发布版本中,我两次得到 2000 > 2000。

第二个补遗:我第一次错过了关于CSng的部分。这更有趣:在发布版本中,CSng 版本是唯一以“2000 <= 2000”结尾的版本。

在调试版本中,从反汇编中可以清楚地看出行为:CSng 调用导致中间 fstp 指令,因此它与对 c 的赋值相同(堆栈位置与变量名没有关联)。

在发布版本中更有趣。在第一个和第三个测试中,compiler/JITter 非常聪明,在编译时进行比较(它实际上并没有将比较调用写入输出,它只写入它知道将采用的分支) .然而,第二个测试结果与调试版本相同,显然是由于 CSng 调用。

Bruce Dawson 详细介绍了浮点数。我强烈建议阅读他关于 x87 浮点计算中的内部精度的文章,链接在这里:https://randomascii.wordpress.com/2012/03/21/intermediate-floating-point-precision/

以上都是 32 位程序的故事,通常更喜欢编译为 x87 浮点数。在 64 位中,一切都通过 SSE/SSE2,它的行为符合 IEEE 浮点数,并且您得到的结果与您可能期望的结果完全相同。前两个的反汇编指令序列如下所示:

vmovss   xmm0, dword ptr [a]
vdivss   xmm0, xmm0, dword ptr [b]
vucomiss xmm0, dword ptr [const 2000]

对于第三个,它只是在指令序列中添加了一个 vmovss dword ptr [c], xmm0 但它在其他方面是相同的(特别是,与 x87 序列不同,它不执行 store/load 对,它仅存储并继续使用注册值)。