Двоичные дроби.
29/03/2006Сегодня snake по аське прислал код:
$start = 0.0; $end = 1.0; $i = 0.1; $x = $start; while ($x != $end) { $x += $i; } echo $x;
Этот код не работает как ожидается (не выводит 1), а зацикливается.
Возмущенные такой несправедливостью, мы полезли искать в мануал, чтобы выяснить, что там говорится о сравнениях дробных чисел. Сам мануал умалчивает о подобном поведении, но в нескольких комментариях говорится о необходимости приведения к строке чисел с плавающей точкой перед их сравниванием, а так же о том, что сравнивание дробных чисел - это "проблема многих языков".
В общем, dragon попробовал на c++, а еще один коллега - на Delphi (pascal). И везде результат одинаков - зацикливает.
В чем дело:
Приводим 0.1 к двоичной системе счисления: 0.110 == 0.(00001)2 Обратите внимание на период в числе в двоичной системе счисления. Итак, получается, сколько не прибавляй 0.(00001)2 к 0.110 никогда не получишь ровно 1.0.
Возьмем в качестве шага цикла (переменная $i) 0.510. Запускаем код - все работает. Приводим к двоичной системе счисления: 0.510 == 0.012. Периода в числе нет.
В качестве теста можно попробовать следующее:
while (round($x,1) != $end) { $x += $i; }
Таким образом, это "проблема" не какого-либо конкретного языка, а принципов хранения и обработки информации в компьютере.
March 30th, 2006 at 11:51 am
Вааще-то, как ни крути, но более безопасный вариант, даже если и не рассматривать вышеозвученный баг, это не проверять в условии равенство значений друг другу, а сравнивать их “больше/меньше”. Тогда и баги ни по чём =)
May 6th, 2006 at 4:46 am
Дело не в баге, а в принципе того как компьютер работает с дробями, советую почитать об этом в какомнибудь учебнике. В общем суть в том что при сравнивании надо делать типа такого: while ($end-$x)
May 6th, 2006 at 6:24 am
2Splix
А я и не говорил о баге. Слово “проблема” взято в кавычки. И рассказано, почему такое происходит - внимательнее читайте
May 6th, 2006 at 3:45 pm
Да я скорее дед морозу
да и вообще сонный был
May 15th, 2006 at 12:18 am
Это классическая проблема. Связана она с особенностями хранения чисел с плавающей точкой (для чисел с фиксированной точкой такой проблемы нет) и возникающей при этом погрешности. Решается, она, например так:
while(abs($x - $end) > 0.01) {
$x += $i;
}
(0.01 - число, заведомо большее чем возможная погрешность, но значительно меньшее чем $i).
March 16th, 2007 at 7:32 pm
Совершенно малозначительное дополнение, практически не относящееся к сути проблемы, но все же: 0.1[dec] это 0.(00011) base 2[bin], а не 0.(00001) base 2[bin] как указано автором.