【问题标题】:Strange compiler behavior with float literals vs float variables浮点文字与浮点变量的奇怪编译器行为
【发布时间】:2010-06-21 20:42:40
【问题描述】:

我注意到 C# 编译器的浮点舍入/截断的有趣行为。也就是说,当浮点字面量超出保证的可表示范围(7 个十进制数字)时,a) 将浮点结果显式转换为浮点数(语义上不必要的操作)和 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 会产生这样的影响吗?另请注意,如果浮点 f 中的最后一个(已经无法表示的)数字被删除甚至减少,那么一切都“到位”。

【问题讨论】:

  • 我在这里看到了这个问题的答案,但找不到

标签: c# compiler-construction floating-point


【解决方案1】:

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

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);

【讨论】:

  • 所以你说原因是 (int) 是通过截断来完成的,而 (float) 是四舍五入的意思。如果是这样,那么为什么 Console.WriteLine((int) (2.0499999f * 100f)) 和 Console.WriteLine((int) (float) (2.0499999f * 100f)) 的输出不同?
  • @Alan,检查我的答案。原因是 float 只能容纳 7 位数字。日志 (2^23) = 6.9
  • @Alan:当您使用硬编码常量时,计算完全在编译器中完成并使用编译器的规则,而不是在 .NET 运行时。
  • @Andrey,谢谢。我知道浮点数超出了可表示的范围(请参阅问题,我没有编辑该部分),但是,经过您的确认,这对我来说有点吓人 - 将浮点数转换为浮点数应该没有什么区别,但是在这个情况,确实如此。
  • @Mark:这些规则有什么不同吗?如果是,我是否应该从 C# 语言参考文档或 MSDN 中知道这一点,或者这只是编译器和运行时之间的偶然差异?
【解决方案2】:

在你问的评论中

这些规则有什么不同吗?

是的。或者,更确切地说,规则允许不同的行为。

如果是,我是否应该从 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

【讨论】:

  • 埃里克,非常感谢。你的最后一个链接特别有启发性。在发布问题之前,我实际上搜索了类似的场景,但显然我的范围太窄了。
【解决方案3】:

关于编译器,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 不能存储为浮点数,因为它只能保存 7 个基于 10 的数字。这个文字是 8 位数字,所以编译器将它四舍五入,因为无法存储。 (应该给 IMO 一个警告)

如果您更改为2.049999f,结果将是预期的。

【讨论】:

  • 谢谢安德烈,我根据编译器与运行时信息选择了马克的回复,但你的也很相关。
猜你喜欢
  • 2021-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多