Число с плавающей точкой в Java (
float и
double) хранится по стандарту IEEE 754 (
JLS, раздел 4.2.3). У
float 32 бита делятся на знак (1 бит), экспоненту (8 бит со смещением 127) и мантиссу (23 бита плюс неявная ведущая единица); у
double 64 бита: 1, 11 (смещение 1023) и 52. Значение считается как (-1)
знак × 1,мантисса × 2
экспонента - 127. Ниже разбор по битам на примере числа 7,5, у которого биты 0 | 10000001 | 11100000000000000000000, то есть 0x40F00000.
Кратко
float это 32 бита по IEEE 754: 1 бит знака, 8 бит экспоненты, 23 бита мантиссы; double: 1, 11 и 52 бита.
- Экспонента хранится со смещением: из восьмибитного числа вычитаем 127 (у
double 1023). Коды 00000000 и 11111111 зарезервированы: ноль и денормализованные числа, бесконечность и NaN.
- Перед мантиссой подразумевается скрытая единица, поэтому реальная точность
float 24 бита, а не 23.
- Значение это (-1)знак × 1,мантисса × 2степень; пример статьи: 0 10000001 11100000000000000000000 это 1,875 × 22 = 7,5.
- Дроби вроде 0,1 и 0,2 в двоичной системе точно не представимы, поэтому
0.1 + 0.2 == 0.3 в Java ложно; для денег используют BigDecimal.
- Биты числа в Java показывает
Float.floatToIntBits, обратно собирает Float.intBitsToFloat; с Java 17 все вычисления с плавающей точкой строго по IEEE 754.
Содержание:
Изображение: http://pikabu.ru/Вступление
В первые же дни изучения Java я наткнулся на такой любопытный вид
примитивов, как числа с плавающей точкой.
Меня сразу заинтересовали их особенности и, тем более, способ записи в двоичном коде (что взаимосвязано). В отличие от какого-либо диапазона целых чисел, даже в очень малом промежутке (например от 1 до 2) их бесконечное множество. И имея конечный размер памяти, невозможно выразить это множество. Так как же они выражены в двоичном коде и как работают?
Увы, объяснения в
вики и достаточно клевой статьи на хабре
тут не дали мне полного понимания, хотя заложили базу. Осознание пришло лишь после одной статьи-разбора с сайта softelectro.ru (сейчас сайт недоступен) наутро после прочтения.
Экскурс в историю
(
почерпнул из этой статьи на Хабре)
В 60-70 гг, когда компьютеры были большими, а программы маленькими, еще не было единого стандарта вычислений, как и стандарта выражения самого числа с плавающей точкой. Каждый компьютер делал это по-своему, и ошибки были у каждого свои. Но в середине 70-х компания Intel решила сделать новые процессоры с поддерживаемой "улучшенной" арифметикой и заодно стандартизировать ее. Для разработки привлекли профессоров Уильяма Кэхэна и Джона Палмера (нет, не автора книг про пиво). Не обошлось без драм, но все же новый стандарт был разработан. Сейчас этот стандарт называют IEEE 754: первая редакция принята в 1985 году, действующая в 2019-м. Java следует ему с первой версии, с Java 15 язык опирается на редакцию 2019 года (
JLS, раздел 4.2.3), а с Java 17 все вычисления с плавающей точкой всегда строгие по стандарту (
JEP 306), поэтому модификатор
strictfp больше не нужен.
Формат записи числа с плавающей точкой
Еще в учебниках школьного курса все сталкивались с непривычным способом записи очень больших или очень малых чисел вида
1,2×103 или
1,2E3, что равно
1,2×1000 = 1200. Это называется способ записи через экспоненту. В данном случае мы имеем дело с выражением числа по формуле:
N=M×np, где
- N = 1200: получаемое число
- M = 1,2: мантисса, дробная часть без учета порядков
- n = 10: основание порядка. В данном случае и когда речь не идет о компьютерах, основанием выступает число 10
- p = 3: степень основания
Довольно часто основание порядка подразумевается, как
10 и записывают лишь мантиссу и значение степени основания, разделяя их буквой
E. В нашем примере я привел равнозначные записи
1,2×103 и
1,2E3
Если все понятно, и ностальгический экскурс в школьную программу мы закончили, то сейчас рекомендую забыть это, т. к. при формировании числа с плавающей точкой мы имеем дело со степенями двойки, а не десятки, т.е.
n = 2, вся стройная формула
1,2E3 ломается и это здорово сломало мне мозг.
Знак и степень
И что мы имеем? В итоге мы также имеем двоичное число, которое состоит из
мантиссы, части, которую будем возводить в степень, и самой степени. Кроме этого, так же как принято и у
целочисленных типов, в числах с плавающей точкой есть бит, который определяет знак: будет число положительным или отрицательным.
В качестве примера предлагаю рассмотреть тип
float, который состоит из 32 бит. С числами двойной точности
double логика такая же, только бит 64: 1 на знак, 11 на экспоненту (со смещением 1023 вместо 127) и 52 на мантиссу.
Из 32 бит первый, старший, отводится на знак, следующие 8 бит отводятся на экспоненту, степень, на которую будем возводить мантиссу, а остальные 23 бита на мантиссу.
float и double: сравнение форматов
Чтобы дальше не путаться, сведем оба формата в одну таблицу:
| Параметр |
float |
double |
| Всего бит |
32 |
64 |
| Знак |
1 бит |
1 бит |
| Экспонента |
8 бит, смещение 127 |
11 бит, смещение 1023 |
| Мантисса |
23 бита плюс скрытая единица, итого 24 значащих |
52 бита плюс скрытая единица, итого 53 значащих |
| Точность |
около 7 десятичных цифр |
около 15-16 десятичных цифр |
| Максимум |
Float.MAX_VALUE, примерно 3,4×1038 |
Double.MAX_VALUE, примерно 1,8×10308 |
| Литерал в Java |
7.5f |
7.5 (без суффикса всегда double) |
Для демонстрации давайте посмотрим на пример:
![Схема 32 бит числа float: бит знака 0, восемь бит экспоненты 10000001, 23 бита мантиссы 11100000000000000000000]()
С первым битом все очень просто. Если значение первого бита
0, то число, которое мы получим будет
положительным. Если бит равен
1, то число будет
отрицательным.
Следующий блок из 8 бит это блок с экспонентой.
Экспонента записывается как обычное
восьмибитное число, а чтоб получить требуемую степень нам нужно из полученного числа вычесть
127 (это число называют смещением экспоненты).
В нашем случае восемь бит экспоненты это
10000001. Это соответствует числу
129. Если есть вопрос, как это посчитать, то на картинке быстрый ответ. Развернутый можно получить на любом курсе булевой алгебры.
1×27 + 0×26 + 0×25 + 0×24 + 0×23 + 0×22 + 0×21 + 1×20 = 1×128 + 1×1 = 128+1=129
Не сложно посчитать, что максимальное число, которое мы можем получить из этих 8 бит
111111112 = 25510 (подстрочные
2 и
10 означают двоичную и десятеричную системы исчисления)
Однако, если использовать только положительные значения степени (
от 0 и до 255), то полученные числа будут иметь много чисел перед запятой, но не после?
Чтоб получать отрицательные значения степени, из сформированной экспоненты нужно вычитать
127. Таким образом, арифметически диапазон степеней получается
от -127 до 128. Но два крайних кода экспоненты зарезервированы:
00000000 означает ноль и денормализованные числа, а
11111111 бесконечность и NaN, поэтому у обычных чисел степень лежит от -126 до 127.
Если использовать наш пример, то нужная степень будет
129-127 = 2. Пока запоминаем это число.
Мантисса
Теперь о мантиссе. Она состоит из 23 бит, однако в начале всегда подразумевается еще одна единица, на которую биты не выделяются.
Это сделано из соображений целесообразности и экономии. Одно и то же число можно выражать разными степенями, добавляя к мантиссе нули перед или после запятой.
Проще всего это понять с десятичной экспонентой:
120 000 = 1,2×105 = 0,12×106 = 0,012×107 = 0,0012×108 и т.д.
Однако, введя фиксированное число в голове мантиссы мы каждый раз будем получать новые числа. Примем как данность, что перед нашими 23 битами будет еще один с единицей. Обычно этот бит от остальных отделяют точкой, которая, впрочем, ничего не значит. Просто так удобнее.
1 . 11100000000000000000000
![Мантисса со скрытым битом: единица, точка и 23 бита 11100000000000000000000]()
Теперь полученную мантиссу нужно возводить в степень слева направо, уменьшая с каждым шагом степень на одну. Стартуем со значения степени, которое мы получили в результате вычисления, т. е.
2.
(Я специально выбрал простой пример, чтоб не писать каждое значение степени двойки и в приведенной таблице не вычислял их, когда соответствующий бит равен нулю)
1×22 + 1×21 + 1×20 + 1×2-1 = 1×4 + 1×2 + 1×1 + 1×0,5 = 4+2+1+0,5 = 7,5
и получили результат
7,5, правильность можно проверить, например, по
этой ссылке, а можно и прямо в Java: метод
Float.floatToIntBits отдает те же 32 бита в виде
int:
int bits = Float.floatToIntBits(7.5f);
System.out.println(Integer.toBinaryString(bits));
System.out.println(Integer.toHexString(bits));
System.out.println(Float.intBitsToFloat(0x40F00000));
Вывод:
1000000111100000000000000000000
40f00000
7.5
Первая строка это наши биты без ведущего нуля знака: сначала экспонента 10000001, затем мантисса 111 и двадцать нулей. Вторая строка это те же биты в шестнадцатеричном виде, а третья это число, собранное из битов обратно.
Итоги
Стандартное число с плавающей точкой типа
float состоит из 32 бит: первый бит это знак (+ или -), следующие восемь экспонента, следующие 23 мантисса.
По знаку: если бит 0, число положительное. Если бит 1, отрицательное.
По экспоненте: побитно переводим в десятичное число (веса битов слева направо:
128,
64,
32,
16,
8,
4,
2,
1), из полученного числа вычитаем
127, получаем степень, с которой будем стартовать.
По мантиссе: к имеющимся 23 битам спереди дописываем еще один бит со значением 1 и с него начинаем возводить в полученную нами степень, с каждым следующим битом декрементируя эту степень.
That's all folks, kids!
P. S.: В виде домашнего задания, используя эту статью, оставьте в комментариях свои версии, почему при большом количестве арифметических операций с числами с плавающей точкой возникают ошибки точностиВопросы и ответы
Как число float хранится в памяти?
В 32 битах по стандарту IEEE 754: 1 бит знака, 8 бит экспоненты и 23 бита мантиссы. Значение считается как (-1)
знак × 1,мантисса × 2
экспонента - 127. Для 7,5 это 0 | 10000001 | 11100000000000000000000: знак плюс, степень 129 - 127 = 2, мантисса 1,111 в двоичной записи, то есть 1,875, и 1,875 × 4 = 7,5. В шестнадцатеричном виде эти же биты записываются как 0x40F00000.
Зачем из экспоненты вычитают 127?
Чтобы хранить отрицательные степени без отдельного бита знака. Восемь бит дают числа от 0 до 255, а вычитание смещения 127 превращает их в степени от -126 до 127 (крайние коды 0 и 255 зарезервированы под ноль, денормализованные числа, бесконечность и NaN). У
double смещение 1023 при 11 битах экспоненты. Побочный плюс такого хранения: положительные
float можно сравнивать как обычные целые числа.
Что такое скрытый бит мантиссы?
У нормализованного двоичного числа перед запятой всегда стоит единица, как в записи 1,2E3 первая цифра не ноль, поэтому хранить ее не нужно.
float хранит 23 бита после запятой, единица подразумевается, и реальная точность равна 24 битам. Исключение: денормализованные числа с экспонентой 00000000, у них вместо скрытой единицы подразумевается ноль, так они дотягиваются до очень малых значений около нуля.
Чем float отличается от double?
Размером и точностью.
float: 32 бита, 8 бит экспоненты, 24 значащих бита, около 7 верных десятичных цифр, максимум примерно 3,4×10
38.
double: 64 бита, 11 бит экспоненты, 53 значащих бита, около 15-16 цифр, максимум примерно 1,8×10
308. В Java литерал без суффикса (
7.5) имеет тип
double, для
float нужен суффикс
f. По умолчанию берите
double: он точнее, а на современных процессорах не медленнее.
Почему 0.1 + 0.2 не равно 0.3 в Java?
Потому что 0,1 и 0,2 не представимы точно в двоичной системе, как 1/3 в десятичной: в
double они хранятся с ошибкой округления, и сумма печатается как 0.30000000000000004. Сравнивать вещественные числа через
== нельзя: сравнивайте разность с допуском (
Math.abs(a - b) < 1e-9) или используйте
BigDecimal для денег. Это же и подсказка к домашнему заданию автора: ошибка каждого округления накапливается с каждой операцией.
Как посмотреть биты числа float в Java?
Метод
Float.floatToIntBits(7.5f) возвращает
int с теми же 32 битами, а
Integer.toBinaryString их печатает: 1000000111100000000000000000000 (ведущий нулевой бит знака опущен). Обратно:
Float.intBitsToFloat(0x40F00000) вернет 7.5. Для
double есть пара
Double.doubleToLongBits и
Double.longBitsToDouble плюс
Long.toBinaryString.
Читайте также