А. Григорьев - О чём не пишут в книгах по Delphi
Несколько иначе устроен Extended. Кроме количественных отличий добавляется еще и одно качественное: в мантиссе явно указывается первый разряд. Это означает, что мантисса 1010… интерпретируется как 1.01, а не как 1.101, как это было в типах Single и Float. Поэтому если 23-битная мантисса типа Single обеспечивает 24-знаковую точность, а 52-битная мантисса Double — 53-битную, то 64-битная мантисса Extended обеспечивает 64-х, а не 65-битную точность. Соответственно, при денормализованной форме записи первый разряд мантиссы явно содержит 0. За ноль экспоненты принимается значение 16 383.
Тип Real, как уже упоминалось, стоит особняком. Во-первых, в нем биты следуют в другом порядке, а во-вторых, нет денормализованной формы. Мы не будем касаться внутреннего устройства типа Real, т.к. эта информация уже перестала быть актуальной.
3.2.4. "Неполноценный" Extended
Ранее мы отметили, что FPU всегда выполняет все операции в формате Extended, оговорившись при этом, что есть исключение из этого правила. Здесь мы рассмотрим это исключение.
У FPU существует специальный двухбайтный регистр, называемый управляющим словом. Установка отдельных битов этого регистра диктует то или иное поведение при выполнении операций. Прежде всего, это связано с тем, какие исключения может возбуждать FPU. Другие биты этого регистра отвечают за то, как будут округляться числа, как FPU понимает бесконечность, — всё это можно при необходимости узнать из документации Intel. Нас же будут интересовать только два бита из этого слова: восьмой и девятый. Именно они определяют, как будут обрабатываться числа внутри сопроцессора.
Если восьмой бит содержит единицу (так установлено по умолчанию), то десять байтов внутренних регистров сопроцессора будут задействованы полностью, и мы получим "полноценный" Extended. Если же этот бит равен нулю, то все определяется значением бита 9. Если он равен единице, то используется только 53 разряда мантиссы (остальные всегда равны нулю). Если же этот бит равен нулю — только 24 разряда мантиссы. Это увеличивает скорость вычислений, но уменьшает точность. Другими словами, точность работы сопроцессора может быть понижена до типа Double или даже Single. Но это касается только мантиссы, экспонента в любом случае будет содержать 15 бит, так что диапазон типа Extended сохраняется в любом случае.
Для работы с управляющим словом сопроцессора в модуле System описана переменная Default8087CW типа Word и процедура Set8087CW(CW: Word). При запуске программы в переменную Default8087CW записывается то управляющее слово, которое установила система при запуске программы. Функция Set8087CW одновременно записывает новое значение в управляющее слово и в переменную Default8087CW.
Такое поведение этой функции не всегда удобно — иногда бывает нужно сохранить старое значение переменной Default8087CW (впрочем, это несложно сделать, заведя дополнительную переменную). С другой стороны, если значение управляющею слова изменить, не используя Set8087CW (а в дальнейшем мы увидим, что такие изменения могут происходить помимо нашей воли), то с помощью функции Default8087CW просто нет возможности узнать текущее значение управляющего слова. В Delphi 6 и выше появилась функция Get8087CW, позволяющая узнать значение именно контрольного слова, а не переменной Default8087CW. В более ранних версиях существовал единственный способ получить значение этого слова — встроенный в Delphi ассемблер.
Итак, установить значение управляющего слова можно с помощью команды FLDCW, прочитать с помощью FNSTCW. Обе эти команды имеют один аргумент — переменную типа Word. Чтобы, например, установить 53-значную точность, не изменив при этом другие биты управляющего слова нужно выполнить такую последовательность команд:
asm
FNSTCW MyCW
AND MyCW, 0FC00h
OR MyCW, 200h
FLDCW MyCW
end;
Начиная с Delphi 6, в модуле Math появилась еще одна функция, позволяющая устанавливать точность FPU без манипуляции с отдельными битами управляющего слова — SetPrecisionMode. В зависимости от значения аргумента (pmSingle, pmDouble или pmExtended) она устанавливает требуемую точность. Современные сопроцессоры обрабатывают числа с такой скоростью, что при обычных вычислениях вряд ли может возникнуть необходимость в ускорении за счет точности — выигрыш будет ничтожен. Эта возможность необходима, в основном, в тех случаях, когда вычисления с плавающей точкой составляют значительную часть программы, а высокая точность не имеет принципиального значения (например, в 3D-играх). Однако забывать об этой особенности работы сопроцессора не следует, потому что она может преподнести один неприятный сюрприз, о котором чуть позже.
3.2.5. Бесконечные дроби
Из школы мы все помним, что не каждое число может быть записано конечной десятичной дробью. Бесконечные дроби бывают двух видов: периодичные и непериодичные. Примером непериодичной дроби является число π, периодичной — число ⅓ или любая другая простая дробь, не представимая в виде конечной десятичной дроби.
ПримечаниеНапомним, что периодичные дроби — это такие дроби которые содержат бесконечно повторяющуюся последовательность цифр. Например, 1/9=0,11111..., 1/12=0,08333333..., 1/7=0,142857142857... Такие числа записывают со скобками — в них заключают повторяющуюся часть. Те же числа должны быть записаны так: 1/9=0,1(1), 1/12=0,08(3), 1/7=0,1(428571)
Вопрос о периодичности или непериодичности числа нас сейчас не интересует, нам достаточно знать, что не все числа можно представить в виде конечной десятичной дроби. При работе с такими числами мы всегда имеем не точное, а приближенное значение, поэтому ответ получается тоже приближенным. Это нужно учитывать в своих расчетах.
До сих пор мы говорили только о десятичных бесконечных дробях. Но двоичные дроби тоже могут быть бесконечными. Даже более того, любое число, выражаемое конечной двоичной дробью, может быть также выражено и десятичной конечной дробью. Но существуют числа (например, 1/5), которые выражаются конечной десятичной дробью, но не могут быть выражены конечной двоичной дробью. Это и есть наиболее важное отличие аппаратной реализации вещественных чисел от наших интуитивных представлений. Теперь у нас достаточно теоретических знаний, чтобы перейти к рассмотрению конкретных примеров — "подводных камней", приготовленных вещественными числами.
3.2.6. "Неправильное" значение
Самый первый "подводный камень", на котором спотыкаются новички — это то, что вещественная переменная может получить не совсем то значение, которое ей присвоено. Рассмотрим это на простом примере (листинг 3.9, примеp WrongValue на компакт-диске).
Листинг 3.9. Пример присваивания "неправильного" вещественного значенияprocedure TForm1.Button1Click(Sender: TObject);
var
R: Single;
begin
R := 0.1;
Label1.Caption = FloatToStr(F);
end;
Что мы увидим, когда нажмем кнопку? Разумеется, не 0.1, иначе не было бы смысла писать этот пример. Мы увидим 0.100000001490116, т.е. расхождение в девятой значащей цифре. Из справки по Delphi мы знаем, что точность типа Single — 7–8 десятичных разряда, так что нас, по крайнем мере, никто не обманывает. В чем же причина? Просто число 0,1 не представимо в виде конечной двоичной дроби, оно равно 0,0(0011). И эта бесконечная двоичная дробь обрубается на 24-х знаках; мы получаем не 0,1, а некоторое приближенное число (какое именно — см. выше). А если мы присвоим переменной R не 0.1, а 0.5? Тогда мы получим на экране 0.5, потому что 0.5 предоставляется в виде конечной двоичной дроби. Немного поэкспериментировав с различными числами, мы заметим, что точно представляются те числа, которые выражаются в виде m/2n, где m, n — некоторые целые числа (разумеется, n не должно превышать 24, а то нам не хватит точности типа Single). В качестве упражнения предлагаем доказать, что любое целое число, для записи которого хватает 24-х двоичных разряда, может быть точно передано типом Single.
ПримечаниеЕсли в этом примере изменить тип переменной R с Single на Double или на Extended, на экран будет выведено 0.1. Но это не значит, что в переменную будет записано ровно 0.1 — это просто особенности работы функции FloatToStr, которая не учитывает столь малую разницу между 0,1 и переданным ей числом.
3.2.7. Сравнение
Теперь попробуем сравнить значение переменной и константы, которую мы ей присвоили (листинг 3.10, пример Compare1 на компакт-диске).
Листинг 3.10. Пример ошибки при сравнении вещественной переменной и константыprocedure TForm1.Button1Click(Sender: TObject);