为什么只有在不调试的情况下此循环才无休止?

时间:2019-02-22 10:02:24

标签: c# floating-point precision

下面的程序进行一些计算,以计算需要多少个无限收敛和的项才能超过某个阈值。我了解/怀疑如果速率太大(例如750,请参阅下文),则这样的循环可能不会终止,因为浮点运算会导致计算不准确。

但是,下面的循环在调试模式下(Microsoft Visual Studio .net 4.6.1)输出i = 514,但在发布模式下不会终止(“挂起”)。也许更奇怪:如果我在循环内注释掉了“ if”部分(只是想弄清楚发生了什么),那么发布模式代码突然也会输出i = 514

这背后的原因是什么?如何避免在发布模式下弹出此类问题? (编辑:我宁愿不在生产代码中添加if语句或break语句;该代码应尽可能地具有高性能。)

 static void Main(string[] args)
    {     
   double rate = 750;
        double d = 0.2;
        double rnd = d * Math.Exp(rate);
        int i = 0;
        int j = 0;
        double term = 1.0;
        do
        {
            rnd -= term;
            term *= rate;
            term /= ++i;
            //if (j++ > 1000000)
            //{
            //    Console.WriteLine(d + " " + rate + "  " + term);
            //    j = 0;
            //    Console.ReadLine();
            //}
        } while (rnd > 0);             
        Console.WriteLine("i= "+i);//do something with i
        Console.ReadLine();
        return;
 }

2 个答案:

答案 0 :(得分:4)

执行摘要,即使在Debug中,代码也被破坏了-即使循环退出,它也会产生错误的结果。您需要了解浮点运算的局限性。

如果您使用调试器逐步执行代码,则会很快发现问题所在。

Math.Exp(rate)大。很大。大于双精度数可以容纳的数字。因此,rnd以值Infinity开始。

来到rnd -= term时,该数字是Infinity减去某个数字,该数字仍为Infinity。因此rnd > 0始终为true,因为Infinity大于零。

这一直进行到term也到达Infinity为止。然后rnd -= term成为Infinity - Infinity的{​​{1}}。与NaN相比的任何事物都是错误的,因此NaN突然是错误的,并且循环退出。

我不知道为什么释放模式会发生这种变化(我无法重现),但是浮点运算的顺序很可能发生了变化。如果您同时处理大量数据,这可能会对输出产生巨大影响。例如,可以对rnd > 0进行排序,以使term *= rate; term /= ++i始终首先在Debug中发生,并且在除法发生之前乘法达到term * rate。在发行版中,可能会重新排序,使得Infinity首先发生,这使您无法再击中rate / ++i。因为您开始时会遇到Infinity始终为rnd的错误,所以只有当Infinity也是term时,循环才会中断。

我怀疑这也可能取决于您的处理器等因素。

编辑:有关更好的解释,请参见@HansPassant的This answer

同样,尽管挂起与未挂起之间的差异可能取决于Debug vs Release,但您的代码从一开始就没有起作用。即使在Debug中,它也会产生错误的结果!

如果要处理大数或小数,则需要注意双精度的限制。浮点数是复杂的野兽,并且具有许多微妙的行为。您需要注意这一点,例如,请参见以下著名文章:What Every Computer Scientist Should Know About Floating-Point Arithmetic。注意局限性,将大号和小号组合在一起时遇到的问题等。

如果您在浮点数的极限附近工作,则需要检验您的假设:例如,确保您没有处理太大或太小的数字。如果您期望指数小于Infinity,请进行测试。如果希望退出循环,请添加保护条件,以确保一定次数的迭代后退出并返回错误。 测试您的代码!确保在极端情况下行为正确。

此外,在适当的地方使用大数字库。如果可能,请对算法进行重新设计,使其对计算机更友好。编写许多算法是为了让数学家在教科书中编写它们很优雅,但是对于处理器执行它们却是不切实际的。经常有一些版本具有相同的功能,但对计算机更友好。

  

我不想在生产代码中添加if语句或break语句;该代码应尽可能地具有高性能。)

不要害怕循环中有一个if语句。如果总是产生相同的结果-例如您的突破永无休止-分支预测器很快就会流行起来,并且分支几乎没有成本。这是一个带有分支的循环,您需要特别注意这一点。

答案 1 :(得分:-1)

  

如何避免在发布模式下弹出此类问题?

始终在循环中包含一个限制。即

if (i > 100_000) break;

您已经注意到,浮点运算在边缘处有些不可预测。与Release / Debug模式的链接不是很清楚,但是优化器可以像这样更改结果。