浮点文字与浮点变量的奇怪编译器行为

时间:2010-06-21 20:42:41

标签: c# compiler-construction floating-point

我注意到C#编译器浮动舍入/截断有趣的行为。也就是说,当浮点字面值超出保证的可表示范围(7个十进制数字)时,则a)显式地将float结果转换为float(语义上不必要的操作)和b)将中间计算结果存储在局部变量中都会改变输出。一个例子:

using System;

class Program
{
    static void Main()
    {
        float f = 2.0499999f;
        var a = f * 100f;
        var b = (int) (f * 100f);
        var c = (int) (float) (f * 100f);
        var d = (int) a;
        var e = (int) (float) a;
        Console.WriteLine(a);
        Console.WriteLine(b);
        Console.WriteLine(c);
        Console.WriteLine(d);
        Console.WriteLine(e);
    }
}

输出结果为:

205
204
205
205
205

在我的计算机上的JITted调试版本中,b的计算方法如下:

          var b = (int) (f * 100f);
0000005a  fld         dword ptr [ebp-3Ch] 
0000005d  fmul        dword ptr ds:[035E1648h] 
00000063  fstp        qword ptr [ebp-5Ch] 
00000066  movsd       xmm0,mmword ptr [ebp-5Ch] 
0000006b  cvttsd2si   eax,xmm0 
0000006f  mov         dword ptr [ebp-44h],eax 

而d计算为

          var d = (int) a;
00000096  fld         dword ptr [ebp-40h] 
00000099  fstp        qword ptr [ebp-5Ch] 
0000009c  movsd       xmm0,mmword ptr [ebp-5Ch] 
000000a1  cvttsd2si   eax,xmm0 
000000a5  mov         dword ptr [ebp-4Ch],eax 

最后,我的问题:为什么输出的第二行与第四行不同?额外的fmul会产生这样的差异吗?另请注意,如果浮动f中的最后一个(已经无法代表的)数字被删除甚至减少,则所有内容都“落实到位”。

3 个答案:

答案 0 :(得分:5)

您的问题可以简化为询问为什么这两个结果不同:

float f = 2.0499999f;
var a = f * 100f;
var b = (int)(f * 100f);
var d = (int)a;
Console.WriteLine(b);
Console.WriteLine(d);

如果查看.NET Reflector中的代码,您可以看到上面的代码实际编译好像是以下代码:

float f = 2.05f;
float a = f * 100f;
int b = (int) (f * 100f);
int d = (int) a;
Console.WriteLine(b);
Console.WriteLine(d);

不能总是精确地进行浮点计算。 2.05 * 100f的结果并不完全等于205,但由于舍入误差而略微减少。当此中间结果转换为整数时,将被截断。当存储为浮点数时,它会舍入到最接近的可表示形式。这两种舍入方法会产生不同的结果。


关于你在写这篇文章时给我答案的评论:

Console.WriteLine((int) (2.0499999f * 100f));
Console.WriteLine((int)(float)(2.0499999f * 100f));

计算完全在编译器中完成。上面的代码相当于:

Console.WriteLine(204);
Console.WriteLine(205);

答案 1 :(得分:4)

在评论中你问过

  

这些规则有何不同?

是。或者说,规则允许不同的行为。

  

如果是,我应该从C#语言参考文档或MSDN中知道这一点,或者这只是编译器和运行时之间的偶然差异

规范暗示了这一点。浮点运算具有必须满足的某个最低精度级别,但如果认为合适,则允许编译器或运行时使用 more 精度。当您执行放大小变化的操作时,这可能会导致大的,可观察到的更改。例如,舍入可以将极小的变化变成非常大的变化。

这一事实导致了这里经常被问到的问题。有关此情况的某些背景以及可能产生类似差异的其他情况,请参阅以下内容:

Why does this floating-point calculation give different results on different machines?

C# XNA Visual Studio: Difference between "release" and "debug" modes?

CLR JIT optimizations violates causality?

https://stackoverflow.com/questions/2494724

答案 2 :(得分:2)

Mark对编译器是正确的。现在让我们欺骗编译器:

    float f = (Math.Sin(0.5) < 5) ? 2.0499999f : -1;
    var a = f * 100f;
    var b = (int) (f * 100f);
    var c = (int) (float) (f * 100f);
    var d = (int) a;
    var e = (int) (float) a;
    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.WriteLine(c);
    Console.WriteLine(d);
    Console.WriteLine(e);

第一个表达式没有意义,但会阻止编译器优化。结果是:

205
204
205
204
205
好的,我找到了解释。

2.0499999f不能存储为float,因为它只能容纳7个基于10的数字。这个文字是8位数,所以编译器舍入它,因为无法存储。 (应该给IMO发出警告)

如果您更改为2.049999f,则可以预期结果。