我注意到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中的最后一个(已经无法代表的)数字被删除甚至减少,则所有内容都“落实到位”。
答案 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?
答案 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
,则可以预期结果。