Двоичные дроби.

29/03/2006

Сегодня snake по аське прислал код:

<?php
  $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. Периода в числе нет.

В качестве теста можно попробовать следующее:

<?php
  while (round($x,1) != $end) {
    $x += $i;
  }
?>

Таким образом, это "проблема" не какого-либо конкретного языка, а принципов хранения и обработки информации в компьютере.

Комментарии: 6 to “Двоичные дроби.”

  1. DeadMoroz сказал:

    Вааще-то, как ни крути, но более безопасный вариант, даже если и не рассматривать вышеозвученный баг, это не проверять в условии равенство значений друг другу, а сравнивать их “больше/меньше”. Тогда и баги ни по чём =)

  2. Splix сказал:

    Дело не в баге, а в принципе того как компьютер работает с дробями, советую почитать об этом в какомнибудь учебнике. В общем суть в том что при сравнивании надо делать типа такого: while ($end-$x)

  3. baranoff сказал:

    2Splix
    А я и не говорил о баге. Слово “проблема” взято в кавычки. И рассказано, почему такое происходит - внимательнее читайте :)

  4. Splix сказал:

    Да я скорее дед морозу :) да и вообще сонный был

  5. Mike сказал:

    Это классическая проблема. Связана она с особенностями хранения чисел с плавающей точкой (для чисел с фиксированной точкой такой проблемы нет) и возникающей при этом погрешности. Решается, она, например так:

    while(abs($x - $end) > 0.01) {
    $x += $i;
    }

    (0.01 - число, заведомо большее чем возможная погрешность, но значительно меньшее чем $i).

  6. _VlaD_ сказал:

    Совершенно малозначительное дополнение, практически не относящееся к сути проблемы, но все же: 0.1[dec] это 0.(00011) base 2[bin], а не 0.(00001) base 2[bin] как указано автором.

BlogMemes.ru