C# 浮点数无限循环

15
在我的机器上,以下C#(.Net 3.5 SP1)代码是一个无限循环:
for (float i = 0; i < float.MaxValue; i++) ;

它达到了数字16777216.0,而16777216.0 + 1的计算结果为16777216.0。然而在这一点上:i + 1!= i。

这有些疯狂。

我意识到浮点数存储存在一定的不准确性。我已经阅读过大于2^24的整数无法以浮点数正确存储的情况。

即使该数字无法正确表示,上面的代码在C#中应该是有效的。

为什么它不起作用?

你可以使用double得到相同的结果,但需要很长时间。9007199254740992.0是double的限制。


7
为什么你在首位使用浮点数类型作为索引? - John
我同意这不是好的代码,但它不应该是正确的代码吗?从技术上讲,除非发生溢出,任何数字加一都应该大于自身。 - jonathanpeppers
1
不一定。您会注意到 16777216.0 是最接近 16777217.0 的单精度浮点数。 - Anon.
1
请查看此链接:http://blogs.msdn.com/ericlippert/archive/2010/01/07/is-there-such-a-thing-as-too-much-precision.aspx - Rune FS
5
《计算机科学家应了解的浮点运算知识》:http://docs.sun.com/source/806-3568/ncg_zh.html。 - jason
在C#中,对于大浮点数,i == i++,因为增量运算符是“#”!!!试试i# :DDD - AndrewBloom
5个回答

21

好的,问题在于要将浮点数加1,需要将其转换为

16777217.0

恰好,这个数字处于进制的边界上,无法完全表示为浮点数。(下一个可用的最高值为16777218.0)

因此,它会四舍五入到最接近的可表示浮点数。

16777216.0

这样说吧:

由于你有一个浮点数的精度,所以你必须逐渐增加一个更高的数字。

编辑:

好吧,这有点难解释,但试试这个:

float f = float.MaxValue;
f -= 1.0f;
Debug.Assert(f == float.MaxValue);
这将正常运行,因为在该值处,为了表示1.0f的差异,需要超过128位的精度。浮点数只有32位。
编辑2
根据我的计算,至少需要128个二进制位无符号数。
log(3.40282347E+38) * log(10) / log(2) = 128

作为解决方案,您可以循环遍历两个128位数字。然而,这将需要至少十年的时间才能完成。


C#应该允许它被递增,对吧?任何浮点数+1都应该比浮点数本身大,对吧?在任何情况下i + 1 > i?如果存在溢出,我会说另外一种情况,但是这个数字远未达到float.MaxValue。 - jonathanpeppers
6
@Jonathan.Peppers:我不确定你是从何处得到关于浮点数系统的这种误解。我建议你在维基百科上了解它们,然后重新评估你的代码。http://en.wikipedia.org/wiki/Floating_point注:该翻译保留了原文中的链接,为了避免改变原意和确保信息完整性。 - Welbog
3
浮点数有固定的数字位数。在接近float.MaxValue时,其值不是精确的,事实上,大部分无关紧要的数字都被截断了。尝试从float.MaxValue中减去1。 - John Gietzen
数字16777216.0远远不及float.MaxValue(3.40282347E + 38)。为什么它会这么快就出现错误? - jonathanpeppers
4
此时此刻,您需要展示您已经阅读了维基百科文章,否则很明显您只是在胡搞。 - Welbog
3
至少读一下John的评论。它功能正常,只是您没有足够的精度来表示10^0增量。 - Shog9

8
例如,假设浮点数由最多2个有效十进制数字和一个指数表示:在这种情况下,您可以准确计数从0到99。接下来是100,但由于您只能有2个有效数字,因此它将被存储为“1.0乘以10的2次方”。将其加一会是什么呢?
最好的情况下,它将作为中间结果为101,但实际上它将通过舍去无关的第3位数字的四舍五入误差再次存储为“1.0乘以10的2次方”。

6

为了理解问题出在哪里,您需要阅读IEEE 浮点数标准。

让我们仔细看一下浮点数的结构:

浮点数被分成两个部分(好吧,其实是三个,但现在先忽略符号位)。

您有一个指数和一个尾数。像这样:

smmmmmmmmeeeeeee

注意:这不是精确的位数,但可以让你大致了解正在发生的事情。
为了确定您拥有的数字,我们进行以下计算:
mmmmmm * 2^(eeeeee) * (-1)^s

那么float.MaxValue会是多少呢?嗯,你将拥有最大可能的尾数和最大可能的指数。假设它看起来像这样:

01111111111111111

实际上我们定义了NAN,+-INF和其他一些约定,但是暂时忽略它们,因为它们与你的问题无关。那么,当你有9.9999*2^99 + 1时会发生什么呢?嗯,你没有足够的有效数字来加1。结果它被舍入为相同的数字。在单精度浮点精度的情况下,+1开始被舍入的点恰好是16777216.0。

2
它与溢出或接近最大值无关。16777216.0的浮点表示为16777216,您将其加1,应该是16777217.0,但是16777217.0的二进制表示为16777216!因此,它实际上没有被递增,或者至少递增并没有达到您的预期结果。
以下是Jon Skeet编写的一个类的示例,可说明此问题:DoubleConverter.cs
请使用以下代码进行尝试:
double d1 = 16777217.0;
Console.WriteLine(DoubleConverter.ToExactString(d1));

float f1 = 16777216.0f;
Console.WriteLine(DoubleConverter.ToExactString(f1));

float f2 = 16777217.0f;
Console.WriteLine(DoubleConverter.ToExactString(f2));

注意,数字16777216.0的内部表示与16777217.0相同!!

(该内容涉及IT技术)

-5
当i逼近float.MaxValue时,迭代会使i略小于该值。下一次迭代会将i加上一个数,但它不能保存比float.MaxValue更大的数字。因此它保存了一个小得多的值,并重新开始循环。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接