Здравствуйте, хабровчане. Я давно увлекаюсь темой регистров с плавающей точкой. Меня всегда волновало то, как происходит вывод на экран и т.д. Помню, давным-давно в универе реализовывал свой класс чисел с плавающей точкой, состоящих из 512 бит. Единственное, что я не мог никак реализовать — это вывод на экран.
Как только у меня появилось свободное время, я взялся за старое. Завел себе тетрадку и пошло-поехало. Хотелось додуматься до всего самому, лишь иногда заглядывая в стандарт IEEE 754.
И вот что из всего этого вышло. Интересующихся прошу под кат.
Чтобы освоить эту статью, надо знать следующее: что такое бит, двоичная система, арифметика на уровне знания отрицательных степеней. В статье не будут затронуты инженерные подробности реализации на уровне процессора а также нормализованные и денормализованные числа. Больший упор сделан на перевод числа в двоичную форму и наоборот, а также объяснение того, как вообще хранятся числа с плавающей точкой в виде битов.
Числа с плавающей точкой — очень мощный инструмент, которым надо уметь правильно пользоваться. Они не столь банальны, как целочисленные регистры, но и не столь сложны, если в них грамотно и потихоньку вникнуть.
В сегодняшней статье для примера я буду использовать 32-битные регистры. Числа с двойной точностью (64-битные) работают абсолютно по той же логике.
Сначала поговорим о том, как хранятся числа с плавающей точкой. Старший 31 бит у нас знаковый. Единичка значит, что число отрицательное, а ноль, соответственно наоборот. Далее идут 8 бит экспоненты. Эти 8 битов представляют из себя обычное беззнаковое число. И в самом конце идут 23 бита мантиссы. Для удобства будем обозначать знак как S, экспоненту как E, а мантиссу, как ни странно, M.
Получаем общую формулу
У мантиссы принято считать один неявный единичный бит. То есть мантисса будет представлять из себя 24 бита, но так как старший 23-й бит всегда единица, то его можно не записывать. Таким образом это «ограничение» будет давать нам единственность представления любого числа.
Мантисса из себя представляет обычное двоичное число, но в отличие от целых чисел, старший бит это 2^0 степени и далее по убыванию степеней. Вот тут и пригождается экспонента. В зависимости от ее значения, степень двойки старшего бита увеличивается либо уменьшается. Вот и вся гениальность этой задумки.
Давайте попробуем показать это на наглядном примере:
Представим число 3.625 в двоичном виде. Поначалу разобьем это число на степени двойки.
Степень старшей двойки равна единице. E – 127 = 1. E = 128.
0 1000000 11010000000000000000000
Вот и все наше число.
Попробуем также и в обратную сторону. Пусть у нас есть 32 бита, произвольные 32 бита.
0 10000100 (1)11011100101000000000000
В скобочках указан тот самый неявный старший бит.
Сначала вычислим экспоненту. E = 132. Соответственно степень старшей двойки будет равна 5. Итого имеем следующее число:
Нетрудно догадаться, что мы можем хранить всего лишь диапазон из 24-х степеней двойки. Соответственно, если два числа отличаются в экспоненте на больше чем 24, то при сложении число остается равным большему среди них.
Для удобной конвертации я накидал небольшую программу на языке C.
Шагом сетки является минимальная разницу между двумя соседними числами с плавающей точкой. Если представить последовательность битов такого числа как обычное целое число, то соседнее число с плавающей точкой будет отличаться в битах как целое число на единицу.
Можно выразиться иначе. Два соседних числа с плавающей точкой будут отличаться на 2 ^ (E — 127 — 23). То есть на разницу, равную значению младшего бита.
В качестве доказательства можете поменять main в коде и скомпилировать еще раз.
Думаю на сегодня можно закругляться, а то получается слишком длинно. В следующий раз буду писать о сложении чисел с плавающей точкой и потере точности при округлении.
P.S.: Я понимаю, что я не затронул тему денормализованных чисел и т.д. Просто не хотелось очень сильно грузить статью, да и эту информацию можно легко найти в стандарте IEEE 754 практически в самом начале.
Как только у меня появилось свободное время, я взялся за старое. Завел себе тетрадку и пошло-поехало. Хотелось додуматься до всего самому, лишь иногда заглядывая в стандарт IEEE 754.
И вот что из всего этого вышло. Интересующихся прошу под кат.
Чтобы освоить эту статью, надо знать следующее: что такое бит, двоичная система, арифметика на уровне знания отрицательных степеней. В статье не будут затронуты инженерные подробности реализации на уровне процессора а также нормализованные и денормализованные числа. Больший упор сделан на перевод числа в двоичную форму и наоборот, а также объяснение того, как вообще хранятся числа с плавающей точкой в виде битов.
Числа с плавающей точкой — очень мощный инструмент, которым надо уметь правильно пользоваться. Они не столь банальны, как целочисленные регистры, но и не столь сложны, если в них грамотно и потихоньку вникнуть.
В сегодняшней статье для примера я буду использовать 32-битные регистры. Числа с двойной точностью (64-битные) работают абсолютно по той же логике.
Сначала поговорим о том, как хранятся числа с плавающей точкой. Старший 31 бит у нас знаковый. Единичка значит, что число отрицательное, а ноль, соответственно наоборот. Далее идут 8 бит экспоненты. Эти 8 битов представляют из себя обычное беззнаковое число. И в самом конце идут 23 бита мантиссы. Для удобства будем обозначать знак как S, экспоненту как E, а мантиссу, как ни странно, M.
Получаем общую формулу
У мантиссы принято считать один неявный единичный бит. То есть мантисса будет представлять из себя 24 бита, но так как старший 23-й бит всегда единица, то его можно не записывать. Таким образом это «ограничение» будет давать нам единственность представления любого числа.
Мантисса из себя представляет обычное двоичное число, но в отличие от целых чисел, старший бит это 2^0 степени и далее по убыванию степеней. Вот тут и пригождается экспонента. В зависимости от ее значения, степень двойки старшего бита увеличивается либо уменьшается. Вот и вся гениальность этой задумки.
Давайте попробуем показать это на наглядном примере:
Представим число 3.625 в двоичном виде. Поначалу разобьем это число на степени двойки.
Степень старшей двойки равна единице. E – 127 = 1. E = 128.
0 1000000 11010000000000000000000
Вот и все наше число.
Попробуем также и в обратную сторону. Пусть у нас есть 32 бита, произвольные 32 бита.
0 10000100 (1)11011100101000000000000
В скобочках указан тот самый неявный старший бит.
Сначала вычислим экспоненту. E = 132. Соответственно степень старшей двойки будет равна 5. Итого имеем следующее число:
Нетрудно догадаться, что мы можем хранить всего лишь диапазон из 24-х степеней двойки. Соответственно, если два числа отличаются в экспоненте на больше чем 24, то при сложении число остается равным большему среди них.
Для удобной конвертации я накидал небольшую программу на языке C.
#include <stdio.h>
union IntFloat {
unsigned int integerValue;
float floatValue;
};
void printBits(unsigned int x) {
int i;
for (i = 31; i >= 0; i--) {
if ((x & ((unsigned int)1 << i)) != 0) {
printf("1");
}
else {
printf("0");
}
if (i == 31) {
printf(" ");
}
if (i == 23) {
printf(" ");
}
}
printf("\n");
}
int main() {
union IntFloat b0;
b0.floatValue = 59.578125;
printBits(b0.integerValue);
b0.integerValue = 0b01000010011011100101000000000000;
printf("%f\n", b0.floatValue);
return 0;
}
Шагом сетки является минимальная разницу между двумя соседними числами с плавающей точкой. Если представить последовательность битов такого числа как обычное целое число, то соседнее число с плавающей точкой будет отличаться в битах как целое число на единицу.
Можно выразиться иначе. Два соседних числа с плавающей точкой будут отличаться на 2 ^ (E — 127 — 23). То есть на разницу, равную значению младшего бита.
В качестве доказательства можете поменять main в коде и скомпилировать еще раз.
union IntFloat b0, b1, b2;
b0.floatValue = 59.578125F;
b1.integerValue = b0.integerValue + 1;
b2.floatValue = b1.floatValue - b0.floatValue;
printBits(b0.integerValue);
printBits(b1.integerValue);
printBits(b2.integerValue);
printf("%f\n", b0.floatValue);
printf("%f\n", b1.floatValue);
printf("%f\n", b2.floatValue);
short exp1 = 0b10000100;
short exp2 =0b01101101;
/* Крайний случай, когда вся мантиса состоит из единиц */
b0.integerValue = 0b01000010011111111111111111111111;
b1.integerValue = b0.integerValue + 1;
b2.floatValue = b1.floatValue - b0.floatValue;
printBits(b0.integerValue);
printBits(b1.integerValue);
printBits(b2.integerValue);
printf("%f\n", b0.floatValue);
printf("%f\n", b1.floatValue);
printf("%f\n", b2.floatValue);
/* Значения экспонент */
printf("%d %d\n", exp1, exp2);
Думаю на сегодня можно закругляться, а то получается слишком длинно. В следующий раз буду писать о сложении чисел с плавающей точкой и потере точности при округлении.
P.S.: Я понимаю, что я не затронул тему денормализованных чисел и т.д. Просто не хотелось очень сильно грузить статью, да и эту информацию можно легко найти в стандарте IEEE 754 практически в самом начале.
Комментарии (10)
amarao
20.06.2019 09:36Эх, и ни слова про ужасы FP. Ненормализованные форматы, множественные нули, бесконечности, nan'ы...
GlumPsyche
20.06.2019 14:56+1Числа с двоичной точностью (64-битные) работают абсолютно по той же логике.
Может, с двойной точностью?
mapron
Автор, вы действительно думаете что младшеклассники читают Хабр?)
Статья отличная! Но не для Хабра, в смысле уровня. Вся статья по сути разжевывание по полочкам вот этой диаграммки:
и формулы
(?1)^s ? m ? 2^exp
Не рассмотрены даже NaN и бесконечности.
berez
А мне понравилось. Диаграммку-то я видел много раз, но вот с разжевыванием по полочкам была проблема. Конкретно — про отрицательные степени двойки в мантиссе и как они соотносятся с экспонентой.
Обычно ограничиваются объяснением типа: «тут мантисса, тут экспонента, тут знак. Эшельме-бешельме — и получается число».
Sultansoy Автор
Статью писал более года назад. Она сейчас из песочницы попала сюда. Поэтому это больше формат песочницы. К тому же эта статья была вводной, далее планировалось разобрать все базовые операции над числами с плавающей точкой, но так как приглашения не получил, решил, что не формат хабра. Возможно в скором будущем продолжу.
К тому же, лично мне самому объясняли по этой диаграмме. Помню, как весь семестр не мог понять, то ли не хотел вникать, то ли объясняли слишком абстрактно. Года 3-4 назад мне очень не хватало именно такой статьи.
mapron
Ну ладно-ладно, я ж не говорю «удоли», раз люди плюсуют статью, значит считают полезным, просто я с этим форматом еще в школе разбирался, у нас продвинутая довольно информатика была)
p.s. выскажу свое фи по поводу «вводных статей» — вводная должна все же нести самостоятельную ценность, помимо какого-то общего обзора. Это только мое мнение. Иначе реально выглядит как недавняя статья про «движок»: «ну вот я может быть такое когда-то напишу, я просто вывалю вам свои планы, а вы мне плюсов отстегните, чтобы замотивировать на следующие части».
Не воспринимайте как личное оскорбление!
Sultansoy Автор
Спасибо большое, учту на будущее
ПыСы. Как оскорбление не воспринимал)