Найти в Дзене
лишние мысли

О машинном представлении вещественных чисел

Все мы в детстве программировали на Pascal-е. Или по крайней мере слышали об этом замечательном языке программирования. Тут, конечно, возникает вопрос "а чем это он, интересно, такой замечательный?", но я позволю себе оставить объяснение за скобками.

Речь пойдет о немного более приземлённых вещах, а именно о том, как в паскалевских типах данных представлены числа.

С натуральными числами более или менее ясно - где-то в глубине машины привычная конструкция
var x : uint64;
...
x := 3462;

превращается в набор нулей и единиц
00000000 00000000 00000000 00000000 00000000 00000000 00001101 10000110
и спокойно хранится в ожидании, когда это значение кому-нибудь понадобится (не будем портить идиллию мыслями о little/big-endian, дополнительном коде и т.п.). А как быть с вещественными числами? Например, вдруг кому-нибудь придет в голову запихнуть в компьютер пресловутое число π?

Тут начинаются сложности. Поскольку память компьютера - штука конечная, то рациональные, алгебраические, а тем более трансцендентные числа приходится как-то урезать. И если с рациональными числами (то есть, представимыми в виде a/b, где a и b - натуральные) ещё можно было бы извернуться и хранить их в виде пары натуральных чисел a и b, то что делать с числом π, совершенно непонятно.

Можно было бы взять монитор широкоформатный, но это кардинально проблему не решит.
Можно было бы взять монитор широкоформатный, но это кардинально проблему не решит.

Однако тут на помощь людям приходят свойства вещественных чисел. Оказывается, множество вещественных чисел сепарабельно, то есть, в нём существует счётное всюду плотное множество. Таким счётным всюду плотным множеством являются вышеупомянутые рациональные числа. Счётность их - факт тривиальный, а по поводу загадочной "всюду плотности" требуется пояснение.

"Всюду плотность" означает, что любое вещественное число можно приблизить рациональными с любой наперед заданной точностью.

Действительно, пусть, например, мы хотим приблизить π рациональным числом с точностью до третьего знака после запятой:
| π − a/b | < 0.001
Возьмём
b = 1000 и решим простенькое неравенство для определения a:
−0.001 < π − a/1000 < 0.001
−1 < π · 1000 − a < 1
−1 < 3141.59... − a < 1
a = 3141
Получили приближение: π ≈ 3141 / 1000

Тот же самый фокус можно проделать, разумеется, и с представлением числа в двоичной позиционной системе счисления. В этой системе число π имеет такой симпатичный вид:
π = 11.00100100001111110110101010001000100001011010001100001...
Если мы хотим опять точность до третьего знака после запятой, то возьмём снова b = 1000 (только на этот раз это не тысяча, а число в двоичном формате!) и получим:
−0.001 < π − a/1000 < 0.001
−1 < π · 1000 − a < 1
−1 < 11001.001... − a < 1
a = 11001

Получили приближение:
π ≈ 11001 / 1000 (напоминаю, всё это - двоичные числа!) или в более привычном десятичном виде: π ≈ 25 / 8. Хуже, конечно, чем в предыдущем случае, но что мы хотели - точность 0.001 в двоичном формате - это всего-навсего 0.125 в десятичном...

Отметим, что оба этих представления, и π ≈ 3141 / 1000, и π ≈ 11001 / 1000 можно записать в чуть боле компактном и единообразном виде:
π ≈ 3141 / 10³, и π ≈ 11001 / 10³
только в первой записи 10 - это обычная десятка, а во второй - "десятка" двоичная, то есть, число 2.

Подобное представление вещественного числа в виде какого-то числа a, умноженного (или поделенного) на основание системы счисления в какой-то степени v, называется экспоненциальной записью вещественного числа. Числа a и v имеют свои названия - "мантисса" и "порядок". То есть, грубо говоря:
экспоненциальная запись = мантисса × (основание в степени порядок)
или, в более компактном виде:
экспоненциальная запись = мантисса E порядок
например, запись 1.23E2 означает 1.23 · 10², то есть, число 123.

Очевидно, что сама по себе такая запись - вещь неоднозначная. Например, число 3462 можно представить и в виде 346.2 · 10¹, и в виде 34.62 · 10². Поэтому общепринятой является так называемая нормализованная запись, в которой мантисса по модулю не превосходит основания:
3462 = 3.462 · 10³
то есть, тут | 3.462 | < 10

И, наконец, разобравшись с терминологией (или окончательно запутавшись), мы подходим к идее представления вещественных чисел в компьютере. Оказывается, они там так и хранятся: в виде своего рационального приближения a/b, где a - некоторое число, b - соответствующая степень двойки.

Вот, например, как выглядит наше число π, засунутое в тип данных Extended:

00110101 11000010 01101000 00100001 10100010 11011010 00001111 11001001 00000000 01000000 (**)

Понять этот частокол из нулей и единиц просто. Число в формате x86 Extended занимает 10 байтов, из которых 64 бита отводятся для хранения мантиссы, т.е., числа a, 15 битов - для хранения порядка, т.е., значения v степени, в которую надо возвести двойку, чтобы получить число b, и один бит - для хранения знака (0 - число положительное, 1 - отрицательное). Получаем:
a → 11001001 00001111 11011010 10100010 00100001 01101000 11000010 00110101
v →
01000000 00000000
(подчеркнутый бит к v отношения не имеет, это - для хранения знака)

Тут возникает небольшая загадка. Если запись числа a совпадает с ранее приведенным представлением π в двоичном виде, то порядок для получения числа b вместо ожидаемого значения 62 содержит какое-то загадочное число 16384. Разгадка проста: разработчики стандарта поделили доступный 15-битный диапазон возможных значений v пополам. Все порядки v, большие 16383, соответствуют числам b - двойкам в степени 63−(v−16383). А все порядки, меньшие 16383, определяют b как двойку в степени 63+(16383−v).

В данном случае, поскольку у нас v - из верхней половины диапазона, то соответствующее число b равно 2 в степени 63−(v−16383) = 62.

Таким образом, значение π, хранящееся в Extended - это рациональное приближение вида a/b, где a = 14488038916154245685 - значение мантиссы соответствующей экспоненциальной записи числа, b = 4611686018427387904 - степень двойки, соответствующая порядку экспоненциальной записи этого числа.

❏❏❏❏❏❏❏❏❏❏❏❏❏

(**) - получить эту цепочку битов легко. Соответствующий код выглядит примерно так:
var
p : Extended;
b : ^Byte;
byte_no, bit_pos : Byte;
...
p := pi;
b := @p;
for byte_no := 0 to 9 do
for bit_pos := 7 downto 0 do
if ((b + byte_no)^ and (1 shl bit_pos)) <> 0 then write(1) else write(0);
writeln;