Разбираем float на части

от автора

Совсем немного теории

Я буду использовать в статье стандартное 32-х битное представление числа IEEE 754 для примера. Другие форматы, в основном отличаются только размером, структура та же. Биты считаются справа налево.

  • 31-й бит — это знак числа, 0 — плюс, 1 — минус

  • с 23-го по 30-й идут биты степени двойки

  • с 0-го по 22-й — дробная часть или мантисса

Знак

Это самое простое, тут не надо ничего придумывать:

int sign = (int) Math.signum(floatNumber); 

Экспонента

Для работы с битами числа, float надо конвертировать в двоичное представление:

int bits = Float.floatToIntBits(floatNumber); 

Чтобы вытащить экспоненту, воспользуемся операцией сдвига. Сначала нужно обнулить 31-й знаковый бит, для этого сдвинем биты влево на 1, затем без учета знака сдвинем на 24 бита вправо, получим 24 нуля слева и само значение:

int exponent = bits << 1 >>> 24; 

Экспонента — это 1 байт кода со сдвигом, минимальное значение -126 представляется нулём, максимальное 127 как 255. Т.е. при кодировании к числу надо прибавить 127, для раскодирования вычесть:

exponent -= 127; 

Дробная часть

Мантисса — это двоичная дробь от 0 до 1 к которой еще прибавляется 1. Для ее раскодирования пройдём от 22-го бита до 0-го, переводя их в десятичный формат. Перевод целого числа осуществляется при помощи умножения разрядов на степень двойки, дробного при помощи деления. Получить значение i-го бита можно так:

  • Сдвинуть 1 на i бит влево, получим степень двойки

  • Выполнить логическое И, тогда на i-м бите будет 0 или 1

  • Сдвинем обратно на i бит вправо, получим десятичные 0 или 1 — bitValue

  • Полученное значение разделать на 2 в степени

Код будет следующий:

float fraction = 1, div = 2; for (int i = 22; i >= 0; i--) {   int bitValue = ((1 << i) & bits) >>> i;   fraction += bitValue / div;   div *= 2; } 

Ограничения точности как раз и происходят из невозможности представить некоторые десятичные дроби в двоичном формате.

Но есть и другой способ. Дробная часть у нас уже есть в двоичном представлении числа, всё что нужно — это вместо экспоненты подставить 0, чтобы степень двойки стала единицей. Сделать это можно следующим образом:

  • Обнулить биты с 23-го по 31-й, сдвинув влево на 9 бит и затем вправо на 9 без учёта знака:

    (bits << 9 >>> 9) = 00000000011100101110010111001100

  • В экспоненту подставить 0 закодированный со сдвигом, для этого 127 сдвинем влево на 23:

    (127 << 23) = 00111111100000000000000000000000

  • Выполнив логическое ИЛИ с этими числами, получим дробную часть с нулевой экспонентой:

float fractionFormula = Float.intBitsToFloat((bits << 9 >>> 9) | (127 << 23)); 

Проверка

Подставим знак, экспоненту и дробь в формулу:

float check = (float)  (sign * Math.pow(2, exponent) * fraction); assert check == floatNumber; assert fractionFormula == fraction; 

Всё вместе

private static void parseFloat(float floatNumber) {   int sign = (int) Math.signum(floatNumber);      int bits = Float.floatToIntBits(floatNumber);   int exponent = bits << 1 >>> 24;   exponent -= 127;      float fraction = 1, div = 2;   for (int i = 22; i >= 0; i--) {     int bitValue = ((1 << i) & bits) >>> i;     fraction += bitValue / div;     div *= 2;   }   float fractionFormula = Float.intBitsToFloat((bits << 9 >>> 9) | (127 << 23));      float check = (float)  (sign * Math.pow(2, exponent) * fraction);   assert check == floatNumber;   assert fractionFormula == fraction;   System.out.println("Binary: " + Integer.toBinaryString(bits));   System.out.printf("parseFloat(%.10f) = %d * 2^%d * %.10f\n", floatNumber, sign, exponent, fraction);     } 

Пример:

parseFloat(2.3164524E-4F); parseFloat(3.6F); 
Binary: 111001011100101110010111001100 parseFloat(0,0002316452) = 1 * 2^-13 * 1,8976378441 Binary: 1000000011001100110011001100110 parseFloat(3,5999999046) = 1 * 2^1 * 1,7999999523 

Другие форматы

Формат IEEE 754 является самым распространённым, но есть и другие, например:

  • Microsoft Binary Format (MBF) — содержит знаковый бит между экспонентой и мантиссой

  • bfloat16 — позволяет увеличить скорость вычислений и сократить место для хранения без значительных потерь в точности

Что ещё почитать

Если вам понравилось двигать биты влево и вправо, рекомендую вот этот фундаментальный труд. То что там описано применяется в жизни. Например, метод вычисления десятичного логарифма числа используется в Java BigDecimal.precision() для получения точности.


ссылка на оригинал статьи https://habr.com/ru/articles/726312/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *