Автор:
И.Е. Тарасов
дата первой публикации 3.08.96
источник
http://www.forth.org.ru/~tarasov/float.txt
сохранено оригинальное форматирование.
Операции с плавающей точкой
"Хорошая программа - документированная программа"
Надпись на памятнике Недокументированной Программе.
Главный аргумент противников Форта (который, к сожалению,
часто бывает справедливым) - "а почему Форт работает только с
целыми числами?". Естественно, бывают задачи, для которых целых
чисел вполне достаточно. Но если все-таки возникла потребность в
использовании плавающей точки, то Форт, на первый взгляд, можно
свернуть в архив, перебросить на дискету и положить ее поглубже
в ящик, а потом занять пару десятков мегабайт на винчестере под
что-нибудь объектно-ориентированное.
Однако одним из основных принципов Форта является отсутс-
твие запретов на что-либо. Следовательно, операции с плавающей
точкой также не запрещаются (Форту просто все равно, что он де-
лает с числами - ADD или FADD). Правда, констатировать возмож-
ность введения таких операций - это одно, а реализовать их на
практике - совершенно другое... Тем не менее, такая реализация
была произведена мной в течение нескольких месяцев, начиная с
апреля 1996 года (можно было сделать и за пару недель, но я за-
нимался не только Фортом, а на Форте - не только плавающей точ-
кой). Для такой надстройки над Фортом был выбран СП-Форт версии
2.02 А.Ю. Черезова. К тому есть несколько причин:
1) Поставляется с исходными текстами.
2) Модель с раздельными сегментами.
3) Машинный код.
4) При написании имелся в виду именно Форт, а не Паскаль
или С++ с синтаксисом Форта (не буду уточнять, чтобы не обидеть,
но в некоторых версиях Форта есть слово, создающее окно на экра-
не, которое при этом снимает со стека порядка десяти параметров
- не проще было вместо Форта заняться библиотечками под С++?).
5) Freeware.
6) Наконец, в этой версии Форта не хватает только плавающей
точки. Все остальное (за редким исключением) там есть. По край-
ней мере, есть возможность сделать, если необходимо. (см. п.1)
Итак, предлагаемая надстройка обеспечивает возможность ис-
пользования в Форте чисел с плавающей точкой. Правда, к машине
предъявляется одно требование - у нее должен быть сопроцессор! А
еще лучше, чтобы это был сопроцессор 80387 и выше. Причина этого
требования очень проста - в качестве стека для чисел с плавающей
точкой используется стек сопроцессора. Операции с этими числами
выполняются также сопроцессором. Эмуляция этих вычислений при
отсутствии сопроцессора пока отсутствует.
Здесь крайне уместно было бы привести немного теории. Стек
сопроцессора представляет из себя восемь регистров. Каждый ре-
гистр имеет размер 10 байт, имеет тэг занятости, и может хранить
1 (одно) число с плавающей точкой в так называемом временном ве-
щественном формате. Вообще же форматов у сопроцессора несколько,
а приведены они в таблице:
Таблица 1
Название Длина, байт Диапазон Как это было бы
(примерно) в С++
Целое слово 2 -32768..32767 short
Короткое целое 4 -10**9..10**9 long
Длинное целое 8 -10**18..10**18 ?
Двоично-десятичное 10 -10**18..10**18 Внутренний
Короткое вещественное 4 -10**38..10**38 float
Длинное вещественное 8 -10**308..10**308 double
Временное вещественное 10 -10**4932..10**4932 long double
Вообще сопроцессор работает только с последним из приведен-
ных форматов. Остальные он использует только при записи чисел в
память и загрузке их оттуда. При этом программист должен явно
указать, в какой именно формат нужно преобразовать число. На ас-
семблере это делается так:
Таблица 2
Название Загрузка Сохранение
Целое слово WORD [BX] FILD WORD [BX] FISTP
Короткое целое DWORD [BX] FILD DWORD [BX] FISTP
Длинное целое QWORD [BX] FILD QWORD [BX] FISTP
Короткое вещественное DWORD [BX] FLD DWORD [BX] FSTP
Длинное вещественное QWORD [BX] FLD QWORD [BX] FSTP
Временное вещественное TBYTE [BX] FLD TBYTE [BX] FSTP
Перед выполнением команды DS:[BX] указывает на первый байт
числа. В данном варианте после сохранения происходит выборка
числа со стека сопроцессора. Чтобы этого не происходило, необхо-
димо использовать команды FIST и FST.
В предлагаемой библиотеке в качестве основного выбрано
длинное вещественное.
При выполнении преобразований обычно происходит потеря точ-
ности. В этом случае получаемый результат округляется сопроцес-
сором по одному из четырех правил:
1) Округление вверх. Результат округляется к плюс бесконеч-
ности.
2) Округление вниз. К минус бесконечности.
3) Округление к нулю. Самый полезный режим (на мой взгляд).
То, что не влезает в новый формат, просто отбрасывается.
4) Округление к ближайшему. В принципе, это должно бы рабо-
тать как обычное математическое округление, т.е. 1.5=2, 1.49=1.
Однако так происходит не всегда, потому что полное название это-
го режима - "округление к ближайшему или четному". Для иллюстра-
ции можно попробовать брать десятичные логарифмы от разных чисел
и округлять их (F[LOG10] в данной библиотеке). Казалось бы,
lg1=0, lg10=1, lg100=2, lg1000=3 и т.д. Но из-за "округления к
четному" это не выполняется. Главные трудности возникают при пе-
чати чисел - их порядок определяется неправильно.
Режим округления задается битами 10 и 11 в двухбайтном ре-
гистре, который называется "управляющее слово сопроцессора" (FPU
Control Word). Там есть и разные всякие другие биты, но пока я
практического применения им не нашел, поэтому не буду описывать
то, что при желании можно найти в справочнике. Итак:
Таблица 3
бит 10 бит 11 Режим
0 0 К ближайшему или четному
0 1 Вниз
1 0 Вверх
1 1 К нулю
В сопроцессоре есть еще один двухбайтный регистр, называе-
мый "слово состояния сопроцессора" (FPU State Word). В нем, кро-
ме всего прочего, хранятся аналоги флажков нуля, переноса, знака
и т.п. Путем некоторых манипуляций эти флажки могут быть скопи-
рованы в обычные флажки процессора, и тогда можно делать услов-
ный переход по результату проверки стека сопроцессора на ноль,
перенос и т.д. Как это делать, можно посмотреть в float.f
Хватит пока теории, нужно и описать то, что позволяет де-
лать Форт после загрузки float.f. Единственным недостатком
СП-Форта, мешающим введению обработки вещественных чисел, явля-
ется отсутствие виртуального слова NUMBER. Оно описано в файле
kernel6.f и его необходимо чуть-чуть подправить. Вот что было
сделано:
1) Перед определением слова NUMBER добавлена строка
VECT NUMBER
2) NUMBER переименовано в SNUMBER. Еще можно было переиме-
новать в NUMBER1 и т.д., но в данном случае S обозначает data
Stack.
3) Добавлена строка
' SNUMBER TO NUMBER
Все. Больше ничего делать не надо (кроме перекомпиляции
Форта и ассемблера). Все остальное берет на себя float.f.
Во-первых, виртуальный NUMBER указывает теперь на FNUMBER, опи-
санный в float.f. Во-вторых, ?LITERAL, который, к счастью, ока-
зался виртуальным, тоже переопределяется, и вещественные числа
можно использовать внутри определений (без этого все возможности
Форта свелись бы к обычному калькулятору).
Возможные форматы чисел:
-1.2345Е0 1.2345Е11 1Е-11 0.00002Е-001
Согласно требованиям стандарта 94-года (предоставленного
мне А.Ю. Черезовым, за что ему оооооооогромное спасибо), опреде-
ляющим признаком "плавательности" числа является наличие буквы
Е. В существующей реализации именно по этой букве и ориентирует-
ся FNUMBER в своей попытке определить, должен ли он преобразо-
вать это число сам, или передать его дальше. Маленький мой не-
догляд, который я пока не стал исправлять - буква должна быть
латинская (ну это везде так) и заглавная (а вот это уже не вез-
де). Тем не менее, хотя так и не было задумано, я все же решил
не бросаться исправлять такое положение вещей. Дело в том, что
стандарт предусматривает, что Форт должен страшно ругаться, если
вдруг кому-то придет в голову поработать с вещественными числа-
ми, если система счисления не десятичная. На мой взгляд, это
крайне спорное утверждение. Ведь если я сначала делаю, скажем
HEX ..........
0305 INPORT \ вполне реальный пример, я именно так и делал
\ вчера
DUP . S>F 10.43E6 F* F.
то скажите, пожалуйста, почему я должен делать еще и DECI-
MAL где-то между INPORT и 10.43Е6 ? Ведь я в случае целочислен-
ных чисел имею в виду адрес порта, который удобнее задавать
шестнадцатиричным, а потом то, что я оттуда читаю, мне нужно ум-
ножить на частоту генератора, которая в целочисленный формат ле-
зет, мягко говоря, с трудом. Естественно, я делаю это на стеке
сопроцессора, где шестнадцатиричная система счисления просто не
имеет смысла ( как бы Вам понравилось увидеть F.12AB56E-2 в ка-
честве результата?). Тем не менее, 10.43Е6 - вполне нормальное
целое число (если забыть про вещественные и вспомнить, что Е=14
в десятичной системе, а точка в Форте - признак того, что число
занимает четыре байта). Целочисленный NUMBER в шестнадцатиричной
системе положил бы на стек четыре байта без раздумий (вернее, с
раздумьями порядка сотен микросекунд). Но если работает FNUMBER,
то строка 10.46Е6 просто до целочисленного NUMBERа не дойдет -
число будет воспринято как вещественное. Именно по этому принци-
пу работает float.f. Но вот если система счисления все-таки
шестнадцатеричная (или хотя бы пятнадцатиричная - чтобы Е было
цифрой), то обычное целое число 1Е3 будет воспринято как 1000. И
все из-за буквы Е ! Выход - набрать 1е3. Тогда FNUMBER не найдет
в строке символа с кодом, равным коду E и число спокойно проплы-
вет на стек данных. Но все это очень нестандартно и описано
только как способ облегчить себе жизнь. Скорее всего, следующая
версия будет гораздо больше соответствовать стандарту.
Итак, FNUMBER здесь есть. Кроме него есть еще FLITERAL, ко-
торый занимается примерно тем же, но в режиме компиляции. Для
целых чисел достаточно скомпилировать инструкции AX, NN MOV и AX
PUSH , где NN - число, любезно предоставленное целочисленным
NUMBERом. Таким образом, если мы используем целое число внутри
определения, то оно как бы хранится в сегменте кода в виде аргу-
мента при непосредственной адресации. С сопроцессором так сде-
лать нельзя, поскольку он должен загрузить на свой стек число из
DS:[BX]. Поэтому FLITERAL делает следующее:
1) Число со стека сопроцессора (его туда поместил FNUMBER)
переносится в HERE.
2) Компилируется инструкция MOV AX, NN, где NN = HERE.
3) Компилируется обращение к FLIT, которое копирует AX в BX
и делает QWORD [BX] FLD.
4) В сегменте данных резервируется 8 байт, чтобы обойти
скомпилированное туда число с плавающей точкой.
Примечание: чтобы скомпилировать не MOV AX, NN и MOV BX,
AX, а сразу MOV BX, NN, надо хотя бы иметь такую возможность в
СП-Форте. Там есть все регистры общего назначения, кроме BX.
Можно, конечно, и это исправить, но тогда придется еще и ker-
nel8.f ломать...
Главное в этом то, что вещественнное число, используемое
внутри определения, хранится уже в сегменте данных. Не вижу в
этом ничего принципиально неприемлемого. Нужно еще учесть, что
сегмент данных растет в СП-Форте гораздо медленнее сегмента ко-
да, а вещественное число занимает 8 байт вместо 2, так что в
медленно растущем сегменте ему самое место.
Теперь, наконец, опишем все слова, добавляемые float.f
(в порядке описания в файле)
FLOAT - создает вещественную переменную; пример - FLOAT X
FDUP - дублирует вершину вещественного стека
FDROP - удаляет число с вершины вещественного стека
FSWAP - меняет местами два верхних числа на вещественном
стеке
S>F - переносит число со стека данных на вещественный стек
F>S - переносит число с вещественного стека на стек данных,
округляя его; округление выполняется сопроцессором
и зависит от установленного режима (см. выше)
F! ( ADR -> ) - снимает с вещественного стека число и запи-
сывает его по адресу, который снимает со стека данных
F@ ( ADR -> ) - снимает со стека данных адрес и загружает
на вещественный стек число, находящееся по нему
GETFPUCW - скопировать управляющее слово сопроцессора в пе-
ременную FPUCW
SETFPUCW - загрузить управляющее слово сопроцессора из пе-
ременной FPUCW
TRUNC-MODE - установить режим округления "к нулю"
ROUND-MODE - установить режим округления "к ближайшему"
F+ - сложить два верхних числа на вещественном стеке
F1+ - прибавить 1 к числу на вершине вещественного стека
F- - вычесть число на вершине вещественного стека из числа,
находящегося под ним
F* - перемножить два верхних числа на вещественном стеке
F/ - разделить второе сверху число на вещественном стеке на
число на вершине
FLOG10 - взять десятичный логарифм от числа на вершине ве-
щественного стека
F[LOG10] - то же, но с округлением (используется при печати
и преобразовании чисел)
F|| - второй вариант "FABS" - модуль числа на вершине
F= - снимает два числа с вещественного стека и кладет на
стек данных (!!!) TRUE, если они равны, или FALSE в
противном случае
F< - "меньше" для вещественного стека
F> - "больше" для вещественного стека
SIN - синус (только для 80387 и выше!!!)
COS - косинус (только для 80387 и выше!!!)
FSQRT - квадратный корень из числа на вершине вещественного
стека
F10X - со стека данных (!!!) снимается число, а на вещест-
венный стек кладется 10 в этой степени (использует-
ся при печати и преобразовании чисел)
F. - снимает с вещественного стека число и печатает его
POSITION ( A,N,C -> P ) - ищет символ C в строке, заданной
адресом первого байта и длиной и кладет его пози-
цию или N+1, если символ не найден (используется
при преобразовании чисел)
COMMAPOS - ищет позицию точки в строке (используется при
преобразовании чисел)
EXP-POS - ищет позицию буквы E в строке (используется при
преобразовании чисел)
SKIP1 ( А -> А ) - удаляет из строки первый символ (исполь-
зуется при преобразовании чисел)
>FLOAT ( А -> ) - пытается преобразовать строку со счетчи-
ком в число с плавающей точкой (если не получается,
ругается самостоятельно)
FLOAT? ( A -> A, T/F ) - кладет поверх адреса TRUE, если
строка со счетчиком по этому адресу может быть числом
с плавающей точкой и система счисления десятичная
FNUMBER - собственно то, ради чего все и затевалось
FLIT ( ADR -> ) - загружает на стек сопроцессора число с
адреса ADR
FLIT, - компилирует вызов FLIT
FLITERAL - компилирует число с плавающей точкой
?FLITERAL - новый вариант LITERALа, работающий еще и с ве-
щественными числами
F, - переносит число с вещественного стека в сегмент данных
FCONSTANT - создает константу с плавающей точкой
PI - кладет на вещественный стек 3.141592 и т.д.
При печати вещественных чисел количество выводимых цифр на-
ходится в переменной FFORM (обычная, а не QUAN). По умолчанию
12. Печать чисел идет через EMIT, поэтому ее также можно пере-
направлять в файл, на принтер и т.д. Кроме FFORM есть еще две
переменные - FPUCW и FPUSW, которые используются для копирования
в них управляющего слова и слова состояния (их можно копировать
только в память). Нужно отметить, что алгоритм печати веществен-
ных чисел отличается от печати целых. В последнем случае строка
для печати формируется начиная с младшего разряда и очередная
цифра получается как остаток от деления числа на основание сис-
темы счисления. С вещественными числами так сделать невозможно -
остатка у них нет. Ввиду этого выделение очередной цифры проис-
ходит следующим образом:
1) Определяется порядок числа (для этого и нужен F[LOG10]).
2) 10 возводится в степень порядка и помещается на вещест-
венный стек.
3) Число делится на 10 в степени порядка.
4) Целая часть от деления и является очередной цифрой для
печати (печатается немедленно).
5) Цифра умножается на десять в степени порядка числа и вы-
читается из числа.
6) Все повторяется FFORM @ раз.
7) Печатается порядок после буквы E.
Пример:
Число 12.23Е1 = 122.3
1) Порядок равен 2.
2) 10 в степени 2 равно 100
3) 122.3/100 = 1
4) 1 печатаем
5) 122.3-1*100 = 22.3
6) 22.3 - новое число, с которым повторяем то же самое.
Поскольку порядок равен 2, печатаем 2 (будет напечатано
1.22300000000Е2).
Как и в большинстве языков, использующих вещественные чис-
ла, иногда печатается внушительное количество девяток после нес-
кольких "нормальных" цифр. Как с этим бороться, не знаю. По
крайней мере, знаю анекдот о том, что 2+2=4, а если есть сопро-
цессор, то 3.999999999999999999999 ...
Здесь же стоит привести алгоритм преобразования строки в
вещественное число.
1) Определяется знак и исключается из строки.
2) От начала строки и до точки (если есть, если нет - до
символа Е) очередная цифра обрабатывается как: F=F+N*10**(-i),
где i - позиция в строке обрабатываемой цифры, N - значение этой
цифры, получаемое по DIGIT.
3) От точки до символа Е - то же самое, но степень не -i, а
-i+1, чтобы учесть наличие точки в строке.
4) После буквы Е подразумевается порядок, который обрабаты-
вается самостоятельно чем-то похожим на целочисленный NUMBER.
5) Выполняется коррекция порядка в соответствии с позицией
точки в строке. Благодаря этому можно вводить 12345.3Е3, а не
только 1.23453Е7, то есть перед точкой может быть любое коли-
чество цифр.
Чуть не забыл самое главное - размер стека. Он равен 8
(поскольку 8 регистров). Однако реально можно использовать 6,
т.к. 2 использует >FLOAT. Печать тоже использует 2 регистра, по-
этому есть опасность потерять данные. Тем не менее если печать
пока не предвидится, стек можно использовать "на всю катушку".
Кстати говоря, это тоже соответствует стандарту, который предпи-
сывает выделять для вещественного стека at least 6 ячеек. Вполне
возможно, что имелся в виду именно сопроцессор.
Несколько слов о том, почему желателен 80387 и выше.
Во-первых, этот самый 80387 и выше спроектирован в соответствии
со стандартом IEEE-754. Но это не так важно. Важно то, что SIN и
COS основаны на командах, которых в 80287 просто нет - там есть
тангенс, а синуса с косинусом вот нет. Кстати говоря, ради этих
двух команд был изменен asm.f. Собственно говоря, их можно прос-
то стереть, если уж требуется совместимость со всеми-всеми-всеми
сопроцессорами.
И последнее. Данная реализация библиотеки поддержки плаваю-
щей точки не является окончательной. Собственно говоря, float.f
- первая нормально работающая реализация. Ввиду этого в ней воз-
можны ошибки, неточности и т.д. Что мог - отловил и исправил.
Исправления, естественно, будут продолжаться, как и добавление
новых слов (сейчас система команд сопроцессора используется да-
леко не полностью). Надеюсь, что моя работа кому-то поможет.