代码之家  ›  专栏  ›  技术社区  ›  Alan

具有浮点文字和浮点变量的奇怪编译器行为

  •  7
  • Alan  · 技术社区  · 15 年前

    我注意到C编译器使用浮点舍入/截断时有一个有趣的行为。也就是说,当float文本超出了保证的可表示范围(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 debug build中,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会有什么不同吗?还要注意的是,如果从float f中删除最后一个(已经不可恢复)数字,甚至减少,所有的“都会到位”。

    3 回复  |  直到 15 年前
        1
  •  5
  •   Mark Byers    15 年前

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

    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);
    
        2
  •  4
  •   Community CDub    8 年前

    在你的评论中

    这些规则不同吗?

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

    如果是的话,我是否应该从C语言参考文档或msdn中了解这一点,或者这只是编译器和运行时之间偶尔的差异?

    规范暗示了这一点。浮点运算的精度必须达到一定的最低水平,但允许编译器或运行时使用 更多 如果合适,则精确。当你进行放大微小变化的操作时,这会导致大的、可观察的变化。例如,舍入可以将极小的变化转化为极大的变化。

    这一事实导致这里出现了相当频繁的问题。有关这种情况和其他可能产生类似差异的情况的一些背景,请参见以下内容:

    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

        3
  •  2
  •   Andrey    15 年前

    马克对编译器的看法是正确的。现在让我们愚弄编译器:

        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 不能以浮点形式存储,因为它只能容纳7个10位数字。这个文字是8位数字,所以编译器对它进行了四舍五入,因为无法存储。(应该警告IMO)

    如果你改成 2.049999f 结果将是预期的。