Почему этот цикл бесконечен только тогда, когда я не отлаживаю его? - PullRequest
0 голосов
/ 22 февраля 2019

Ниже программа делает некоторые вычисления, чтобы вычислить, сколько членов некоторой бесконечной сходящейся суммы необходимо, чтобы превысить определенный порог.Я понимаю / подозреваю, что подобные циклы могут не завершиться, если скорость слишком велика (например, 750, см. Ниже) из-за неточностей вычислений, вызванных арифметикой с плавающей запятой.

Тем не менее, ниже цикла выводит i = 514 в режиме отладки (Microsoft Visual Studio .net 4.6.1), но не завершается ("зависает") в режиме выпуска.Возможно, еще более странно: если я закомментирую часть «если» внутри цикла (предназначенную для выяснения того, что происходит), тогда код режима освобождения неожиданно также выдаст 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 голосов
/ 22 февраля 2019

Краткое содержание Ваш код не работает даже в Debug - он дает неверный результат даже при выходе из цикла.Вы должны знать ограничения арифметики с плавающей точкой.

Если вы пошагово просматриваете свой код с помощью отладчика, вы быстро видите, что не так.

Math.Exp(rate) велико.Очень большой.Может быть больше, чем число с двойной точностью.Поэтому rnd начинается со значения Infinity.

Когда вы переходите к rnd -= term, это Infinity минус некоторое число, которое все еще равно Infinity.Поэтому rnd > 0 всегда верно, поскольку Infinity больше нуля.

Это продолжается до тех пор, пока term также не достигнет Infinity.Тогда rnd -= term становится Infinity - Infinity, что составляет NaN.Все, что сравнивается с NaN, является ложным, поэтому rnd > 0 внезапно становится ложным, и ваш цикл завершается.

Я не знаю, почему это меняется в режиме выпуска (я не могу воспроизвести его), но этоВполне возможно, что порядок ваших операций с плавающей точкой был изменен.Это может сильно повлиять на результат, если вы имеете дело как с большими, так и с маленькими числами одновременно.Например, term *= rate; term /= ++i может быть упорядочен так, что term * rate всегда происходит первым в Debug, и это умножение достигает Infinity, прежде чем произойдет деление.В Release это может быть переупорядочено таким образом, что rate / ++i происходит первым, и это останавливает вас от попадания Infinity.Поскольку вы начали с ошибки, что rnd всегда Infinity, ваш цикл может прерваться, только если term также Infinity.

Я подозреваю, что это может зависеть от таких факторов, как ваш процессор,а также.

РЕДАКТИРОВАТЬ: См. Этот ответ @HansPassant для гораздо лучшего объяснения.

Итак, еще раз, в то время как разница между зависанием ине зависание может зависеть от Debug vs Release, ваш код никогда не работал в первую очередь.Даже в Debug он дает неправильный результат!

Если вы имеете дело с большими или маленькими числами, вам нужно быть осторожным с ограничениями двойной точности.Числа с плавающей точкой - сложные звери, и имеют много тонкого поведения.Вы должны знать об этом, см., Например, эту знаменитую статью: Что должен знать каждый учёный-компьютерщик об арифметике с плавающей точкой .Помните об ограничениях, проблемах с объединением больших и малых чисел и т. Д.

Если вы работаете вблизи пределов с плавающей запятой, вам необходимо проверить свои предположения: убедитесь, что вы ненапример, иметь дело с числами, которые слишком велики или слишком малы.Если вы ожидаете, что возведение в степень будет меньше Infinity, проверьте это.Если вы ожидаете, что цикл завершится, добавьте условие защиты, чтобы убедиться, что оно завершается с ошибкой после определенного числа итераций. Проверьте свой код! Убедитесь, что он правильно работает в крайних случаях.

Кроме того, при необходимости используйте библиотеку больших чисел.Если возможно, переделайте свой алгоритм, чтобы он был более дружественным к компьютеру.Многие алгоритмы написаны так, что математики могут изящно писать в учебнике, но их непрактично выполнять процессору.Часто есть версии, которые делают то же самое, но более удобны для компьютера.

Я бы не стал добавлять оператор if или break в производственный код;этот код должен быть как можно более быстрым.)

Не бойтесь одного оператора if в цикле.Если он всегда дает один и тот же результат - например, ваш разрыв никогда не срабатывает - предсказатель ветвления очень быстро срабатывает, и ветвление почти не требует затрат.Это цикл с веткой, который непредсказуем, с которым нужно быть осторожным.

0 голосов
/ 22 февраля 2019

Как избежать появления подобных проблем в режиме релиза?

Всегда включайте в свой цикл ограничение.Т.е.,

if (i > 100_000) break;

Как вы заметили, арифметика с плавающей запятой немного непредсказуема по краям.Связь с режимом Release / Debug не очень понятна, но оптимизатор может изменить результаты, как это.

...