на школьную страницу...

Коварный float

С.И.Хашин

Какие бывают числа
Тип Размер
в байтах
макс.
значение
BYTE 1 255
short 2 32767
int 4 2 млрд.
int_64 8 0.9*1019
float 4 3.4*1038
double 8 1.7*10308
long double(extended) 10 103000
Замечание. 10-байтовые плавающие числа являются «родными» для Intel-овских процессоров. То есть сам процессор работает лишь с такими числами. Даже если ваша программа работает с 4-х или с 8-ми байтовыми числами, при загрузке в процессор из памяти они преобразовываются в 10-байтовые и все операци выполняются именно с ними. И лишь при записи в память числа снова преобразовываются к требуемому (8-ми или 4-х байтовому) виду.

По не совсем понятным причинам Microsoft ведет борьбу с 10-байтовыми числами. Начиная с конца 90-х годов все микрософтовские компиляторы делают вид, что ничего о таких числах не знают.

В последние годы все большее развитие получают графические сопроцессоры и некоторые другие аналогичные приспособления (SSE* и др.). Для них «родными» являются уже как 4-х, так и 8-ми байтовые числа.

Сколько получится, если сложить 50 единиц?
50.
А 50 миллионов?
50 000 000.
А если это переменная типа float? Проверим, на всякий случай.

    float s=0;
    for( int k=0; k<50; k++ ) s++;
    printf("%f\n", s);
 50.000000

Здесь всё нормально.

    float s=0;
    for( int k=0; k<50000000; k++ ) s++;
    printf("%f\n", s);
 16777216.000000

Интересно, правда? С чего бы это?
Может быть, такие большие числа не помещаюся в число типа float?
Не может быть! Предел для них равен 3.4*1038, до него ещё очень далеко!
Так что же тогда мешает? Оказывается, точности, разрядности не хватает.

Переменная типа float занимает в памяти 4 байта, её значение можно представить в виде (мантисса)*2(степень), причем мантисса - трехбайтовое число.
Поэтому, если s≥16 777 216, то 1 оказывается так мала по сравнению с s, что s+1 не примерно равно s, а точно:

s+1==s

Поэтому сколько ни прибавляй единицу, число s не изменится!

Конечно, единицы складывать не очень интересно. Давайте рассмотрим более реальный пример.
Яркость каждой точки на изображении является целым числом от 0 до 255. Допустим, нам даны 100 миллионов точек с яркостями

f(k),k=0,..., 99 999 999.
Как найти среднюю яркость? Попробуем так:
    int N=100000000, int_s=0;
    for( int k=0; k<N; k++ ) int_s+=f(k);
    int_s /= N;
    printf("%d\n", int_s);

Получится плохо, потому, что целые (32-разрядные) числа могут принимать значения не более 2 млрд., а наша сумма будет, скорее всего, намного больше. А значит, - результат получится неверный. Например, если каждое из f(i) равно 128, то сумма будет равна -84901888, а среднее - ноль.

Попробуем использовать числа типа float:

    int N=100000000;
    float s=0;
    for( int k=0; k<N; k++ ) s+=f(k);
    s /= N;
    printf("%d\n", s);

В результате получим сумму 2 147 483 648 и среднее значение 21.474836 (каждое из f(k) опять было равно 128). Уже лучше, по крайней мере среднее значение положительно. Но опять не то.

И только при использовании double результат будет именно тот, который и должен быть.

Вывод: нельзя использовать float для нахождения суммы более, чем 10 млн. слагаемых.

А на сколько безопасен в этом случае double? Ведь наверное, если слагаемых будет побольше, то и он «сломается»?
Конечно, сломается. Но если слагаемых будет больше 256 ≈ 0.7•1017. Если считать, что мы складываем по 2 млрд. слагаемых в секунду (оптимистичный вариант), то до этого предела мы будем добираться более года. Так что если ожидаемое время работы программы меньше года, то об этой проблеме можете забыть.

Flag Counter