9.5. Использование математического сопроцессора 80X87
Применение сопроцессора значительно увеличивает точность математических расчетов и ускоряет их выполнение. Дело лишь в малом — наличии его и умении использовать.
- 181 -
Чтобы программа могла задействовать возможности сопроцессора, она должна в своем начале иметь директиву (ключ режима компилятора) $N+ :
{$N+ программа для 80X87}
PROGRAM Name;
USES
...
Режим компиляции $N+ является глобальным и не может меняться в дальнейшем. При создании модулей (UNIT), ориентированных на работу с сопроцессором, т.е. использующих вводимые им типы, указание ключа $N+ в них необязательно. Важно лишь, чтобы он был в главной программе, включающей в себя эти модули.
При необходимости отключить сопроцессор должен указываться ключ $N-. При этом программа может перестать компилироваться (компилятор «забудет» типы чисел с повышенной точностью).
Если в ПЭВМ установлен сопроцессор, то компилятор сам определяет ключевое слово CPU87 для условной компиляции. Это можно использовать для автоматического выбора режима компиляции:
{$IFDEF CPU87} {$N+} {$ELSE} {$N-} {$ENDIF}
Приведенная выше конструкция определяет, как будет скомпилирован текст — в расчете на сопроцессор или без него.
После компиляции выполнение программы всякий раз начинается с проверки наличия сопроцессора и определения его типа. Результат проверки записывается в предопределенную переменную системной библиотеки — Test8087 типа Byte (табл. 9.8).
Таблица 9.8
Значения Test8087
Расшифровка
0
Сопроцессор не обнаружен
1
Подключен 8087
2
Подключен 80287
3
Подключен 80387
Если программа компилировалась в режиме {$N+}, а значение Test8087 получилось равным 0, то программа остановится с выдачей сообщения о необходимости сопроцессора. Существует, однако, средство отключить автоматическую проверку наличия сопроцессора при запуске программы. Надо ввести системную переменную MS-DOS с именем 87 и значениями Y (от YES — да) и N (от NO — нет). Лучше всего это сделать в файле AUTOEXEC.BAT, вставив строку
- 182 -
SET 87=Y (или N)
Значение системной переменной MS-DOS 87, равное Y, прикажет считать сопроцессор подключенным, а N — соответственно отключенным. Вообще говоря, лучше не обманывать технику и программы. Приберегите эту методику (с SET 87=) на самый крайний случай.
Турбо Паскаль дает возможность эмулировать работу сопроцессора программным путем. Это означает, что можно создать программу, которая будет работать с высокой точностью независимо от наличия сопроцессора. Обнаружен сопроцессор — хорошо, он и будет нагружен, нет сопроцессора — вся точность будет получена имитацией его. Понятно, что в последнем случае будут потери во времени счета, и немалые. Включением эмуляции управляет ключ $Е. Он имеет смысл только рядом с ключом $N. Возможны такие их сочетания:
{$N+, E+} — подключение библиотеки для эмуляции сопроцессора; при его отсутствии точность обеспечивается программно за счет скорости; {$N+, E-} — программа сможет работать только на машинах с сопроцессором;
{$N-,E+} и {$N-,E-} — сопроцессор не используется, ключ эмуляции игнорируется. Программа работает только с обычной точностью и скоростью.
Если в программу вставлен внешний код директивой {$L Имя-Файла.OBJ}, то для работы с сопроцессором этот код должен быть получен с учетом использования инструкций 80X87.
Преимущества от использования сопроцессора — это, в первую очередь, скорость вычислений, которая может вырасти в несколько раз. Второе преимущество — увеличение точности вычислений с плавающей точкой. В расчете на математический сопроцессор вводятся типы, приведенные в табл. 9.9.
Таблица 9.9
Тип
Диапазон значений
Количество значащих цифр
Размер в байтах
Single
1.5E-45..3.4e+38
7-8
4
Double
5.0E-324..1.7E+308
15-16
8
Extended
3.4E-4932..1.1E+4932
19-20
10
Comp
-9.2E+18..9.2E+18
19-20
8
Все эти типы — вещественные, за исключением Comp, который является «очень длинным» целым типом (хранит только целые
- 183 -
значения). Диапазон этого типа в таблице задан округленно, так как реальные числа (от -2 до 263-1) слишком длинны.
Обычный тип Real (6 байт, диапазон 2.9Е-39...1.7Е+38, 11-12 значащих цифр) будет работать с сопроцессором, но крайне неэффективно. Этот формат — чужой для сопроцессора, и время, «съедаемое» преобразованием его в сопроцессорный тип, перекрывает ускорение. А точности не добавляется. Поэтому лучше всего ввести свой тип, например Float, и понимать под ним либо Real, либо чисто сопроцессорный вещественный тип в зависимости от режима компиляции.
{$N... <-- какой-либо режим }
{$IFOPT N+}
TYPE
Float = Double; { или любой другой тип для 80X87 }
{$ELSE>
TYPE
Float = Real; { без 80X87 — только этот тип }
{SENDIF}
VAR { переменные типа Float }
r : Float;
d : Array [1..9] of Float;
Целые типы Турбо Паскаля работают с сопроцессором без каких-либо оговорок.
Особо важным является вопрос точности вычислений. При использовании сопроцессора все стандартные математические операторы и функции языка, возвращающие обычно значения Real, начинают возвращать значения типа Extended. В связи с этим имеет смысл опираться именно на этот тип как базовый. Тем не менее вполне возможно, что в программе будут участвовать переменные разных типов. В таких случаях при необходимости будет производиться преобразование значений, а значит, потеря точности. При вычислении значений правых частей операторов присваивания результат имеет точность, совпадающую с наиболее точным из типов членов выражения (или, что то же самое, с наиболее емким типом). Это означает, что в присваивании
VAR
e1, e2 : Extended;
e3 : Double;
result : Single;
...
result := e1*e2/e3;
- 184 -
значение выражения справа будет вычислено как тип Extended. Но при присваивании его переменной result «малого» типа Single будет произведено усечение, и резко уменьшится число значащих цифр после десятичной точки. Подобные ситуации надо предвидеть и стараться избегать их. Особенно неприятны они в циклах суммирования:
VAR
е : Extended:
Sum : Single;
i : Word;
...
BEGIN
e:=1.23456e-12;
Sum:=0;
for i:=32767 to 65535 do Sum := Sum + i/e;
...
END.
Здесь подобные потери будут повторены тысячи раз, и накопленная ошибка может быть соизмерима с самой суммой. Исправить ситуацию легко: надо ввести дополнительную переменную eSum точного типа Extended для сумматора, и переписать цикл:
eSum:=0;
for i:=32767 to 65535 do eSum := eSum + i/e;
Sum:=eSum;
Теперь потери будут значительно меньше.
По той же причине (из-за усечения точности) некорректной является операция сравнения двух разнотипных вещественных переменных или переменной с выражением (последнее, как уже отмечалось, может быть вычислено в типе Extended). Так, сравнение в примере:
VAR
e : Extended;
d : Double;
...
e := Cos( Pi/8 );
d := e;
{==>} if d=e then ...
при формальной правильности и очевидности даст результат False — ложно, так как d имеет меньше значащих цифр, чем e. Обычно при сравнении вещественных значений проверяют не их совпадение, а степень расхождения. Если эта степень соизмерима с точностью представления наиболее грубого числа, то значения можно считать
- 185 -
равными. Так, условие if в последнем примере следовало бы переписать так:
if Abs(d-e) < 1.0Е-15 then ...
Здесь 1.0Е-15 — точность для типа переменной d (Double).
Продолжим перечень особенностей применения сопроцессора. Его наличие в ПЭВМ и использование сильно влияет на работу функции округления Round: она начинает округлять полуторные значения в сторону ближайшего четного целого числа (это называется «банковским способом»)! Например:
без сопроцессора с сопроцессором
Round(0.5) --> 1 Round(0.5) --> 0
Round(1.5) --> 2 Round(1.5) --> 2
Round(2.5) --> 3 Round(2.5) --> 2
Round(3.5) --> 4 Round(3.5) --> 4
С остальными значениями (без '.5') функция работает нормально.
Некоторые неприятности могут поджидать любителей рекурсивного подхода к написанию функции. Возможны, в принципе, ситуации, когда рекурсивные вызовы переполнят внутренний стек данных сопроцессора, рассчитанный на восемь уровней рекурсии, и возникнет сбой программы. Возможным решением будет разнесение сложнорекурсивных выражений типа Fn:=Fn(N-1)+Fn(N-2) по локальным переменным, например, f1:=Fn(N-1); и f2:=Fn(N-2). После этого выражение Fn:=f1+f2 будет безопасным для сопроцессора.
Завершая тему использования сопроцессора, напомним, что и расширенные вещественные типы, и тип Real при работе с сопроцессором 80X87 выводятся на печать операторами Write и WriteLn с 4 цифрами в показателе степени:
при $N- WriteLn(123.4) выдаст 1.2340000000Е+02,
но при $N+ WriteLn(123.4) выдаст 1.234000000000000Е+0002.
Этот факт надо учитывать при форматированном выводе и при преобразовании чисел в строку процедурой Str.
- 186 -