пятница, 20 мая 2011 г.

Численные методы на службе прикладного программиста

Точность чисел
На определённые исследования по этой теме меня сподвиг вопрос теста:
long l1 = Long.MAX_VALUE;
long l2 = Long.MAX_VALUE-1;
double d1 = Long.MAX_VALUE;
System.out.println("l1 == d1: " + (l1 == d1));
System.out.println("l2 == d1: " + (l2 == d1));
Чтобы никто не мучился, сразу скажу, вывод даст:
l1 == d1: true
l2 == d1: true

Почему?

Магия, не иначе. Корень зла в том, что на типы long и double выделено по 8 байт. Вот только у long все они идут на представление мантиссы, а у double нет: 53 бита на мантиссу и 11 на ординату (степень).

Приведу более короткий пример на мелких числах:
Если в long число выглядит как 12345, то в double будет нечто типа 123 * 10^2 = 12300
А всё из-за нехватки разрядов...

Сколько же можно вычитать?
Оказывается, что 511. При 512 ответ уже станет false, что достаточно логично.
Посчитаем сами:
1 бит знаковый. то есть в long остаётся 63, а в double 52.
Разница в 11 бит. Что можно ими закодировать? 512 - это 10 бит. И ещё 1 даёт правильное округление.
2^10 + 1 = 1024. Отсюда следует, что если в long заполнены и нулевой и 62 биты (то есть double не может откинуть с какой-то стороны биты, заменив соответствующей ординатой), то в double эти числа идут с большой потерей точности: на 1024. Учитывая правильное округление, то максимальная ошибка составит 512.
Небольшой эксперимент, позволяющий увидеть, что же происходит:
double d1 = Long.MAX_VALUE;
        double d2 = Long.MAX_VALUE - 511;
        double d3 = Long.MAX_VALUE - 512;
        double d4 = Long.MAX_VALUE - 1024;
        double d5 = Long.MAX_VALUE - 1024-512;
        double d6 = Long.MAX_VALUE - 1024*2;
        double d7 = Long.MAX_VALUE - 1024*2-512;

        long l1 = (long) d1;
        long l2 = (long) d2;
        long l3 = (long) d3;
        long l4 = (long) d4;
        long l5 = (long) d5;
        long l6 = (long) d6;
        long l7 = (long) d7;

        System.out.println(Long.toBinaryString(l1));
        System.out.println(Long.toBinaryString(l2));
        System.out.println(Long.toBinaryString(l3));
        System.out.println(Long.toBinaryString(l4));
        System.out.println(Long.toBinaryString(l5));
        System.out.println(Long.toBinaryString(l6));
        System.out.println(Long.toBinaryString(l7));
        System.out.println(l1);
        System.out.println(l2);
        System.out.println(l3);
        System.out.println(l4);
        System.out.println(l5);
        System.out.println(l6);
        System.out.println(l7);

Результат работы:
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111110000000000
111111111111111111111111111111111111111111111111111110000000000
111111111111111111111111111111111111111111111111111100000000000
111111111111111111111111111111111111111111111111111100000000000
111111111111111111111111111111111111111111111111111010000000000
9223372036854775807
9223372036854775807
9223372036854774784
9223372036854774784
9223372036854773760
9223372036854773760
9223372036854772736

Как видим, самый верхний пример показывает, что шаг в double равен 1023, но это только потому, что нет ещё одного разряда. А вот следующие шаги уже по 1024.

Вывод:
Несмотря на то, что в приведении long к double компилятор даже не требует явного преобразования типов, преобразование это опасно. поэтому нужно внимательнее подходить к таким вещам.

Комментариев нет:

Отправить комментарий