Дата публикации статьи: 02.02.2006 13:47

Устройство типа данных Double в VB6

[Обсудить в форуме]
[Скачать исходный код]

В данной статье речь пойдёт об устройстве числа типа Double, способе представления данных в нём и алгоритмах перевода между человеческим и машинным представлением дробных чисел.
    Что же такое тип Double и каково его отличие скажем от типа Long?
    Прежде всего, это два разных класса чисел, имеющих различный формат хранения данных. Тип Double относится к классу дробных чисел, а тип Long к целым, не имеющим дробной части. Различие это не условное, а настолько кардинальное, что для произведения операций над ними используются два разных процессора. С целыми числами работает центральный процессор, а над дробными математический сопроцессор, известный так же как Floating Point Unit, что в переводе с английского означает устройство обработки чисел с плавающей запятой (или точкой в американской системе метрик).
    Что это означает? Это означает что для того, чтобы математический сопроцессор смог произвести какую то операцию над числом, нужно чтобы это число было представлено в формате с плавающей запятой. Что же это за формат такой?
    Начнём с того, что формат чисел с плавающей запятой является международным стандартом (IEEE 754) представления дробных чисел. Во время своего путешествия по просторам Интернета вы наверняка случайно или намеренно натыкались на списки TOP 50 или TOP 100 самых производительных суперкомпьютеров мира. Для определения производительности такого компьютера используется единица FLOPS, что означает количество операций с плавающей запятой в секунду. Как видите, критерием скорости вычислений являются отнюдь не целые, а как раз дробные числа. Особенностью представления числа с плавающей запятой является раздельное хранение атрибутов числа, но совместное хранение данных. Число с плавающей запятой разбивается на три отсека: это знак, позиция запятой и сами цифры. Для позиции запятой также принято название экспонента, а для цифр название мантисса. Мантисса содержит вместе и дробную и целую часть. Например, число 123.45 хранится в мантиссе как 12345, а где находится точка, определяется экспонентой. Почему в мантиссе данные не разделены? Потому что число в ней нормализовано.     Это значит, что целая часть приведена к единице. Например, число 123.45 в нормализованном виде выглядит как 1.2345*10^2 или, как принято в ЭВМ, 1,2345E+2, что аналогично предыдущему представлению. Как вы уже догадались, буква E обозначает позицию экспоненты. В таком виде целая часть числа всегда будет единицей, поэтому, чтобы сэкономить один бит для повышения точности, эта единица в число не записывается. Это правило может показаться странным человеку, привыкшему к десятичной системе, ведь целое число может быть не только единицей, а скажем двойкой. Действительно, с точки зрения человека вы правы, но в двоичной системе ни двойки, ни тройки не существует, а есть лишь последовательность нолей и единиц, следовательно, любое число выражается через такую последовательность. Если целая часть ноль, то единица, которая всегда присутствует в числе с плавающей запятой, окажется в дробной части. Единица не учитывается только в том случае, если мантисса и экспонента равны нулю, что соответствует нулевому значению и целой и дробной частей.
    Дробные числа в формате с плавающей запятой принято ещё называть вещественными числами. Стоит отметить, что математический сопроцессор использует не один, а целых три представления для чисел с плавающей запятой.
    Максимальная длина числа, которое может обработать сопроцессор - 80 двоичных разрядов (бит). Самое короткое число называется числом одинарной точности (Single) и в длину достигает лишь 32 бита (такая же длина у самого большого целого типа Long). Из этих 32 бит: 1 бит используется для знака, 7 бит для экспоненты и 24 бита для мантиссы. Следующее за ним число имеет уже удвоенную точность (Double) за счёт удвоения количества разрядов этого числа до 64. Из них 1 бит знаковый, 11 бит экспоненты и 52 бита мантиссы. Ну и последнее число, расширенной точности (в VB6 не поддерживается), имеет максимально допустимую общую длину в 80 разрядов с длиной мантиссы в 64 разряда.
    Так как предметом статьи является тип Double, то и рассматривать мы будем только его. Как уже говорилось выше, позиция точки в вещественном числе записывается в экспоненту числа, которая в нашем случае составляет 11 бит.
Значение экспоненты записывается подобно знаковому целому числу, но положительные и отрицательные части при этом меняются местами. Если в целом знаковом числе положительные значения занимают меньшую половину диапазона, а отрицательные большую (обратную форму Intel использует и для записи самого значения), то в вещественном числе данные располагаются уже по возрастанию значений.  

Таблица 1. Используемый Intel формат целого числа со знаком на примере байта.

положительные значения:

от 00000000 до 01111111

в двоичной системе

отрицательные значения:

от 10000000 до 11111111

в двоичной системе

 Не будем вдаваться в дискуссии относительно удобства такого представления чисел (это дело привычки), обратим лишь внимание, что при этом ноль в экспоненте (Таблица 2) оказывается не в начале числа, а в середине, следуя сразу же за отрицательным диапазоном.  

Таблица 2. Формат записи диапазона значений в экспоненте.

Отрицательные значения:

до 01111111110 (до 1022 в десятичной)

Ноль:

01111111111 (1023 в десятичной)

Положительные значения:

от 10000000000 (от 1024 в десятичной

Как видите, в таблице 2 изображено три возможных состояния экспоненты:

  • Отрицательная. Меньше нуля в знаковом или меньше 1023 в беззнаковом представлении. Если экспонента отрицательная, то число находится в диапазоне от 0 до 1, т.е. оно больше нуля, но меньше единицы. Чем меньше значение экспоненты, тем меньше число. Характеристикой числа с отрицательной экспонентой является то, что у него нет значащих цифр в целой части, а всё что записано в мантиссе является дробной частью.

  • Нулевая экспонента (1023 в беззнаковом представлении). Целая часть числа с нулевой экспонентой в любом случае равна единице, что соответствует представлению нормализованного числа. Остальные значащие цифры мантиссы являются дробными.

  • Положительная. Больше нуля в знаковом или больше 1023 в беззнаковом представлении. При положительной экспоненте отношение целой и дробной части зависит от значения экспоненты. Чем больше значение, тем больше целая часть, чем меньше значение, тем больше дробная часть.

Теперь давайте немного попрактикуемся и рассмотрим число из документации Intel по сопроцессору.
    Это число 178,125. Чтобы в будущем не возникало недоразумений, сразу скажу вам, что дробь эта конечная. Под словом конечная я подразумеваю что при делении числителя на знаменатель мы не получим периодической дроби. Например, дробь 1/3 в десятичной системе получится периодической.
    Наши же дроби находятся вовсе не в десятичной, а, как и все числа в ЭВМ, в двоичной системе счисления. Следовательно, число периодических десятичных дробей резко возрастает. Об этом мы поговорим ниже, после описания устройства дробей.
Вернёмся к нашему числу 178,125. Целая часть у нас 178, которая переводится в двоичное представление обычным алгоритмом преобразования строки в число (деление на 10 с остатком) и будет выглядеть как 10110010.
    Переведём внимание на дробную часть. Итак, у нас 125 тысячных, т.е. 125/1000. Посмотрите на делитель, он кратен 10. Точно так же в двоичной дроби делитель кратен 2. Например, десятичной десятке соответствует двоичная двойка, сотне(10) соответствует четвёрка(2), а тысяче(10) восьмёрка(2) и т.д. Тем, кто не понял, напомню, что в десятичной системе разряд числа (единицы, десятки, сотни и т.д.) это степень числа 10. Соответственно в двоичной системе это будет степень двойки.
    Знаменателя 1000 в двоичной системе не бывает. Не круглое это число в двоичной системе. В десятичных дробях знаменатели - это всегда степени десятки, значит в двоичных дробях знаменатели (они же круглые числа) - это всегда степени двойки. Проведя аналогию с десятичной системой, найдём знаменатели для разрядов двоичной.  

Таблица 3. Способ записи дробей в десятичной и двоичной системах.

Десятичная:

0.1

0.01

0.001

0.0001

 

1/10

1/100

1/1000

1/10000

Двоичная:

0.1

0.01

0.001

0.0001

обыкновенная

1/2

1/4

1/8

1/16

с основанием 2

1/10

1/100

1/1000

1/10000

Пользуясь таблицей 3, можно уже на глаз прикинуть каким будет знаменатель числа. Какой двоичный эквивалент имеет десятичная 1/1000? Правильно, 1/8. Десятичная 8 в двоичном представлении выглядит как 1000. Двоичная двойка - это 10, четвёрка -100, восьмёрка - 1000 и т.д. В числителе, к сожалению, дела обстоят не так гладко как в знаменателе. Дробь 1/1000 может иметь до 999 различных значений, в то время как 1/8 лишь семь. Помня, что в компьютере числа хранятся в двоичной системе, можно увидеть, насколько велики потери точности при записи десятичных дробей.
    Разделив десятичный знаменатель на двоичный можно увидеть шаг, при попадании в который получатся целые непериодические дроби. 1000/8=125. Иными словами, для знаменателя 1000 в десятичной системе непериодическими будут только те числители, которые кратны двоичному знаменателю 8. Это 0.125; 0.250; 0.375; 0.500; 0.625; 0.750; 0.875. Проверяем:

Таблица 4. Кратность десятичных и двоичных дробей.

125/1000

=

1/8

=

0.001

250/1000

=

2/8

=

0.010

375/1000

=

3/8

=

0.011

500/1000

=

4/8

=

0.100

625/1000

=

5/8

=

0.101

750/1000

=

6/8

=

0.110

875/1000

=

7/8

=

0.111

Аналогично для знаменателя 10000, непериодический шаг будет 10000/16=625. Значит, шаг числителя должен быть 625, чтобы дробь получилась целой.
 
Чтобы лучше понимать устройство двоичных дробей, возьмём последний вариант из Таблицы 4.
 875/1000 = 7/8 = 0.111
    Число 7/8 раскладывается по разрядам знаменателей и может быть представлено как 1/2+1/4+1/8 или 0.1+0.01+0.001=0.111. Приведя слагаемые к общему знаменателю можно сложить дробь обратно. 1/2 - это то же что 2/4 и то же что 4/8. 1/4 - это тоже что 2/8. Итак, общий знаменатель у на сесть - это 8. Теперь, сложа числители, мы вернёмся к изначальной дроби 4/8+2/8+1/8=7/8.
     Перевести десятичную дробь в двоичную можно тем же делением в столбик что и для десятичных чисел, но по правилам двоичного деления. Производя деление в столбик на листке бумаги, когда мы добавляем ноль к делимому, если оно меньше делителя, мы понимаем что число при этом умножается на десять. Аналогично и в двоичном делении в столбик, при добавлении нуля к делимому, оно будет умножаться на 2. Как и в любом переводе между системами, остатки пойдут в результат, а частное будет использовано вновь как делимое.
    Надеюсь, что такое 0.125 вы поняли. Это 0.001 в двоичной. Сложив целую и дробную части числа 178.125 мы получим двоичное 10110010.001
Это ещё не всё, теперь нужно перевести двоичную дробь в формат с плавающей запятой. Для начала нужно привести это число в нормализованный вид, а это значит оставить в целой части только 1. Всё остальное переходит в дробную часть.
10110010.001 = 1.0110010001
    При этом запятая сместилась на 7 порядков в большую сторону. Соответственно смещение это нужно где-то запомнить, чтобы в будущем вернуть запятую на своё место. Эта семёрка сохранится в экспоненте. В знаковом формате экспоненты ноль - это 1023 для вещественного типа Double, значит, семёрка будет 1023+7=1030. Значение экспоненты получено.
    Теперь разберёмся с мантиссой. Как уже говорилось ранее, целая часть нормализованного числа - всегда единица, независимо от числа (кроме нуля и других исключений). Конечно, эта единица может сдвинуться при денормализации числа в целую или в дробную часть (в зависимости от знака экспоненты), но денормализация может понадобиться только при конвертации из формата с плавающей запятой в формат целых или двоично-десятичных чисел. Главное что единица присутствует всегда, и это подвигло составителей формата IEEE 754 не записывать эту единичку, а принимать её условно.
Следуя этому правилу, уберём 1 из числа 1.0110010001 и получим 0110010001.
    Осталось определится только со знаком. За знак отвечает самый старший 63-й бит. Если бит сброшен, то знак положительный, если установлен - отрицательный. 0-плюс, 1-минус. Итак, знак у нас положительный, значит 0.
    Теперь объединим все три части. Сначала знак - 0, затем 11 бит экспоненты - 1030 или 10000000110, ну и, наконец, 52 бита мантиссы - 0110010001. Получается значение:
0 10000000110 0110010001000...0
Именно в таком виде и хранится число 178.125 в формате числа с плавающей запятой.

Автор: CyRax
Сайт:
http://basicproduction.nm.ru