Почему этот цикл никогда не заканчивается?

Возможное дублирование:
проблема при сравнении двойных значений в C #

Я читал это в другом месте, но действительно забыл ответ, поэтому я спрашиваю здесь снова. Кажется, что этот цикл никогда не закончится, независимо от того, кодируете ли вы его на любом языке (я тестирую его на C #, C ++, Java ...):

double d = 2.0;
while(d != 0.0){
   d = d - 0.2;
}

person Truong Ha    schedule 28.07.2010    source источник
comment
Никогда не используйте == с плавающими значениями. Может, использовать что-нибудь вроде f>epsilon.   -  person pascal    schedule 28.07.2010
comment
Прошло несколько дней, полагаю, срок истек.   -  person GManNickG    schedule 28.07.2010
comment
В Java, я думаю, есть модификатор Strictfp для таких ситуаций.   -  person Mahmoud Hanafy    schedule 28.07.2010
comment
@ Фобия: strictfp не поможет. Это проблема, присущая всем числам с плавающей запятой, независимо от размера или точности. Единственный способ обойти это - использовать базу, отличную от 2, для экспоненты, и я не знаю ни одной реализации, которая это делает.   -  person cHao    schedule 28.07.2010
comment
@cHao: Машины IBM Power поддерживают пересмотренный стандарт IEEE 754: 2008, который определяет десятичные типы с плавающей запятой. Используя double, код не работал бы даже там; использование вместо этого 'decimal64' (decimal32, decimal128) - с тем или иным написанием - будет работать точно. (См. Также SO 1447215.)   -  person Jonathan Leffler    schedule 29.07.2010


Ответы (9)


Вычисления с плавающей запятой не совсем точны. Вы получите ошибку представления, потому что 0.2 не имеет точного представления в виде двоичного числа с плавающей запятой, поэтому значение не становится точно равным нулю. Попробуйте добавить оператор отладки, чтобы увидеть проблему:

double d = 2.0;
while (d != 0.0)
{
    Console.WriteLine(d);
    d = d - 0.2;
}
2
1,8
1,6
1,4
1,2
1
0,8
0,6
0,4
0,2
2,77555756156289E-16   // Not exactly zero!!
-0,2
-0,4

Один из способов решить эту проблему - использовать тип decimal.

person Mark Byers    schedule 28.07.2010
comment
А почему бывают круглые ошибки? Тип double хранит двоичные цифры. Поскольку 0,2 записанного двоичного кода является периодической дробью, мы должны что-то отрезать, чтобы записать число на N бит. - person adf88; 28.07.2010

(Во-первых, вы не используете одну и ту же переменную повсюду, но я предполагаю, что это опечатка :)

0,2 на самом деле не 0,2. Это ближайшее значение double к 0,2. Когда вы вычтете это 10 раз из 2,0, вы не получите точно 0,0.

В C # вы можете использовать вместо этого тип decimal, который будет работать:

// Works
decimal d = 2.0m;
while (d != 0.0m) {
   d = d - 0.2m;
}

Это работает, потому что десятичный тип действительно точно представляет десятичные значения, такие как 0,2 (в определенных пределах; это 128-битный тип). Каждое задействованное значение точно представимо, поэтому оно работает. Что не сработает, так это:

decimal d = 2.0m;
while (d != 0.0m) {
   d = d - 1m/3m;
}

Здесь «третья» не совсем представима, поэтому мы сталкиваемся с той же проблемой, что и раньше.

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

У меня есть статьи о двоичной плавающей запятой и десятичная точка с плавающей запятой из контекста C # /. NET, которые объясняют вещи более подробно.

person Jon Skeet    schedule 28.07.2010

Я помню, как купил Sinclair ZX-81, проработал отличное руководство по программированию Basic и почти вернулся в магазин, когда наткнулся на свою первую ошибку округления с плавающей запятой.

Никогда бы не подумал, что 27,99998 лет спустя у людей все еще будут эти проблемы.

person yosser    schedule 28.07.2010
comment
Руководство ZX-Spectrum содержит подробное объяснение этой проблемы. Я помню, как много думал об этом (в возрасте 10 или 11 лет) и сказал: «О, это имеет смысл». Это было очень хорошее руководство ... - person Donal Fellows; 28.07.2010
comment
+1 за 27.99998 лет :-) - person Péter Török; 28.07.2010

Вам лучше использовать

while(f  > 0.0) 

* изменить: см. комментарий Паскаля ниже. Но если вам нужно выполнить цикл целое детерминированное число раз, лучше используйте целочисленный тип данных.

person StuartLC    schedule 28.07.2010
comment
Я знаю, что должен его использовать, но почему f! = 0.0 не работает? - person Truong Ha; 28.07.2010
comment
Вы можете запустить один раз слишком много, если последний f равен 2.0e-16 ... - person pascal; 28.07.2010

Проблема в арифметике с плавающей запятой. Если для числа нет точного двоичного представления, вы можете сохранить только ближайшее к нему число (точно так же, как вы не можете сохранить число 1/3 в десятичном виде - вы можете хранить что-то вроде 0.33333333 только для некоторой длины '3'). Это означает, что арифметические действия с числами с плавающей запятой довольно часто не совсем точны. Попробуйте что-нибудь вроде следующего (Java):

public class Looping {

    public static void main(String[] args) {

        double d = 2.0;
        while(d != 0.0 && d >= 0.0) {
            System.out.println(d);
            d = d - 0.2;
        }

    }

}

Ваш результат должен быть примерно таким:

2.0
1.8
1.6
1.4000000000000001
1.2000000000000002
1.0000000000000002
0.8000000000000003
0.6000000000000003
0.4000000000000003
0.2000000000000003
2.7755575615628914E-16

И теперь вы должны понять, почему условие d == 0 никогда не возникает. (последнее число - это число, которое очень близко к 0, но не совсем.

В качестве другого примера странностей с плавающей запятой попробуйте следующее:

public class Squaring{

    public static void main(String[] args) {

        double d = 0.1;
        System.out.println(d*d);

    }

}

Поскольку двоичного представления точно 0.1 не существует, возведение в квадрат не дает ожидаемого результата (0.01), но на самом деле что-то вроде 0.010000000000000002!

person Stephen    schedule 28.07.2010
comment
Никогда полностью не преувеличиваю, ИМО. То, что не каждое десятичное значение точно не может быть представлено в двоичной системе с плавающей запятой, не делает сложение точно представленных 0,25 и 0,25, чтобы получить точно 0,5, например, менее точным. - person Jon Skeet; 28.07.2010
comment
Отредактировал, спасибо Джону - это было немного завышено. - person Stephen; 28.07.2010

f не инициализирован;)

Если ты имеешь ввиду:

double f = 2.0;

Это может быть эффектом неточного артиметика на двойные переменные.

person Piotr Müller    schedule 28.07.2010

это из-за точности с плавающей запятой. используйте while (d> 0,0) или, если необходимо,

while (Math.abs(d-0.0) > some_small_value){

}
person Louis Rhys    schedule 28.07.2010

Как уже говорили другие, это просто фундаментальная проблема, с которой вы сталкиваетесь при выполнении арифметических операций с плавающей запятой с любой базой. Просто так получилось, что base-2 является наиболее распространенным в компьютерах (потому что допускает эффективную аппаратную реализацию).

Лучшее исправление, если возможно, - это переключиться на использование некоторого частного представления числа для вашего цикла, сделав на основе этого значение с плавающей запятой. Хорошо, это звучит преувеличенно! Для вашего конкретного случая я бы написал это как:

int dTimes10 = 20;
double d;
while(dTimes10 != 0) {
   dTimes10 -= 2;
   d = dTimes10 / 10.0;
}

Здесь мы действительно работаем с дробями [20/10, 18/10, 16/10, ..., 2/10, 0/10], где итерация выполняется с целыми числами (т.е. легко получить правильные значения) в числитель с фиксированным знаменателем перед преобразованием в числа с плавающей запятой. Если вы можете переписать свои настоящие итерации так, чтобы они работали таким образом, вы добьетесь большого успеха (и в любом случае они не намного дороже, чем то, что вы делали раньше, что является отличным компромиссом. чтобы получить правильность).

Если вы не можете этого сделать, вам нужно использовать для сравнения равенство внутри эпсилона. Примерно это замена d != target на abs(d - target) < ε, где выбор ε (эпсилон) иногда может быть неудобным. По сути, правильное значение ε зависит от множества факторов, но, вероятно, лучше всего выбрать 0,001 для примера итерации с учетом масштаба значения шага (т. Е. Это половина процента от величины шага, поэтому все, что в пределах этого будет ошибкой, а не информативной).

person Donal Fellows    schedule 28.07.2010

Это не останавливается, потому что 0.2 i не точно представлен в двух дополнениях, поэтому ваш цикл никогда не выполняет тест 0.0==0.0

person raf    schedule 28.07.2010
comment
Дополнение до двух не имеет никакого отношения к ошибкам округления чисел с плавающей запятой. - person Egon; 18.11.2012