Страница Ивана Рощина > Статьи >

© Иван Рощин, Москва

ZXNet: 500:95/462.53
E-mail: bestview@mtu-net.ru
WWW: http://www.ivr.da.ru

Передача параметров при вызове ассемблерных процедур из программы на Бейсике

Радиомир. Ваш компьютер» 10/2001)
Дата последнего редактирования: 26.03.2003.

Введение
Переменные Бейсика
Процедура определения адреса переменной
Пример оптимизации
Несколько полезных советов
Создание новых переменных
Литература

Введение

После того, как программа на Бейсике написана, часто выясняется, что она работает непозволительно медленно. Есть несколько способов решения этой проблемы. Можно полностью переписать программу на ассемблере, добившись при этом максимального быстродействия, — но на это придётся затратить много времени и сил. Можно использовать компилятор Бейсика — это не потребует сколько-нибудь существенных усилий, но программа не будет столь же быстрой, как написанная на ассемблере, к тому же могут возникнуть проблемы с недостатком памяти при компиляции или при попытке запуска откомпилированной программы. Можно, наконец, переписать на ассемблере только те участки программы, на выполнение которых тратится большая часть времени. При этом быстродействие будет лишь немногим ниже по сравнению с полным переписыванием программы на ассемблере, а объём работы будет значительно меньше. Именно этот способ оптимизации в ряде случаев оказывается наиболее предпочтительным.

При использовании ассемблерных процедур встаёт вопрос передачи параметров. В стандартном Бейсике средства для этого весьма скромны: предусмотрено лишь, что вызываемая процедура может вернуть в регистровой паре BC одно значение, представляющее собой целое число в диапазоне 0—65535. Скажем, при выполнении команды

    ....
    1000 LET A = USR 40000
    ....

будет вызвана ассемблерная процедура, располагающаяся по адресу 40000, после чего переменной A будет присвоено значение регистровой пары BC.

Можно выделить для передачи параметров определённую область памяти, записывать туда перед вызовом процедуры значения передаваемых переменных с помощью оператора POKE, а после вызова — считывать возвращаемые значения с помощью оператора PEEK. Пусть, скажем, требуется передать процедуре масштабирования координат, находящейся по адресу 40004, переменные X и Y (целые, в диапазоне 0—65535) через область памяти 40000—40003 (с адреса 40000 располагается значение X, в формате «младший байт, старший байт»; с 40002 — значение Y, в таком же формате). Процедура изменит эти значения определённым образом, оставив результат в тех же ячейках памяти; после этого надо прочесть возвращённые значения и присвоить их переменным X1 и Y1.

    .....
    1000 POKE 40000,X-INT(X/256)*256: POKE 40001,INT(X/256)
    1010 POKE 40002,Y-INT(Y/256)*256: POKE 40003,INT(Y/256)
    1020 RANDOMIZE USR 40004
    1030 LET X1=PEEK(40000)+PEEK(40001)*256
    1040 LET Y1=PEEK(40002)+PEEK(40003)*256
    .....

Как видим, при таком способе передачи параметров увеличивается объём бейсик-программы, тратится время на передачу значений (иногда довольно значительное — например, при передаче массивов), приходится запоминать, по какому адресу какая переменная находится, да и, наконец, просто может не хватить памяти.

Более перспективным способом представляется тот, когда ассемблерная программа непосредственно работает с переменными Бейсика. Рассмотрим этот способ более подробно.

Переменные Бейсика

Рассмотрим сначала, какие бывают в Бейсике переменные, где и в каком формате они хранятся.

Переменные бейсик-программы находятся в области памяти, начало которой адресуется системной переменной VARS, расположенной по адресу #5C4B=23627. Переменные идут строго друг за другом, а после последней из них находится байт #80 — признак конца области переменных.

Хотя в именах переменных допускаются и прописные, и строчные латинские буквы, а также пробелы и цифры (цифра не может быть первым символом), в области хранения переменных имена преобразуются к строчным буквам, а пробелы не хранятся (таким образом, для интерпретатора переменные Size X и sizex — одно и то же), знак «$» для символьных переменных также не хранится.

Ниже перечислены различные типы переменных и приведён формат их хранения (данные о формате, приведённые в [1], прямо скажу, не отличаются полнотой и точностью, поэтому многое пришлось изучать в ходе практических экспериментов).

Как видим, по значению первого байта имени переменной сразу же можно определить её тип:

Табл. 1
Диапазон значений первого байта Тип переменной
#41—#5A символьная
#61—#7A число (имя — одна буква)
#81—#9A числовой массив
#A1—#BA число (имя — несколько символов)
#C1—#DA символьный массив
#E1—#FA переменная цикла

Приведу некоторые сведения о пятибайтной форме представления чисел, используемой при хранении переменных. Целые числа в диапазоне 0..65535 представляются следующим образом:

0 0 младший байт числа старший байт числа 0

А целые числа в диапазоне –65535..–1 — так:

0 #FF младший байт числа старший байт числа 0
(число представляется в дополнительном коде)

Вещественные числа хранятся в более сложной форме. Сведения о формате их хранения вы можете почерпнуть, например, из [3]. Здесь я их не привожу, так как работа с такими числами на ассемблере оказывается необходимой лишь в очень редких случаях. Часто можно преобразовать решаемую задачу так, чтобы работать только с целыми числами, что намного ускоряет вычисления.

Попробуем теперь, вооружившись полученными знаниями, «расшифровать» содержимое реальной области переменных, дамп которой приведён на рис. 1.

Рис. 1

Первый байт области переменных — #98. Он принадлежит диапазону #81—#9A, следовательно, первая переменная — числовой массив. Как мы помним, для числового массива код символа имени хранится увеличенным на #20. Определим имя: #98–#20=#78, это код символа «x». В следующих двух байтах находится длина массива (не считая байт имени и два байта самой длины). Прочитав её (#23=35) и перейдя на соответствующее число байтов вперёд, мы можем сразу перейти к следующей переменной. Но пока делать этого не будем, а рассмотрим структуру массива. В следующем байте хранится 2 — значит, массив двумерный. По первому измерению его размер равен 2, по второму — 3 (а всего, значит, в нём 6 элементов). Дальше располагаются сами элементы. X(1,1)=4, X(1,2)=0, последующие элементы также равны нулю.

Первый байт следующей переменной — #C1, следовательно, это символьный массив с именем A$… Ну и так далее; надеюсь, с оставшейся частью этой области переменных вы сможете разобраться самостоятельно.

Процедура определения адреса переменной

Для определения местоположения переменной Бейсика удобно воспользоваться нижеприведённой процедурой. Вот её входные и выходные параметры.

Вход: DE указывает на имя переменной, преобразованное в соответствии с её типом (т.е. имя должно быть именно в таком виде, как оно хранится в области переменных), заканчивающееся байтом 0.

Выход: если переменная найдена — флаг Z сброшен, HL указывает в области переменных на первый байт после имени найденной переменной, DE указывает на следующий байт после 0 (а значит, если хранить имена искомых переменных друг за другом, то, вначале загрузив в DE адрес первого имени и затем последовательно вызывая процедуру, мы будем последовательно получать адреса этих переменных). Если переменная не найдена — флаг Z установлен, DE остаётся без изменений.

Процедура при своей работе изменяет содержимое регистра A.

VARS       EQU  #5C4B

FIND_VAR   LD   HL,(VARS)

           PUSH BC

FIND_M0    PUSH DE
           PUSH HL

;Сравнение имени текущей переменной с именем искомой:

FIND_M1    LD   A,(DE)
           INC  DE

           AND  A
           JR   Z,FIND_YES ;Имена совпали.

           CP   (HL)
           INC  HL
           JR   Z,FIND_M1

;Имена не совпали - пропускаем текущую переменную:

           POP  HL
           POP  DE
           LD   A,(HL)
           INC  HL

           CP   #80       ;Дошли до конца области переменных?
           JR   Z,FIND_NO ;Если да - выходим, флаг Z установлен.

           CP   #60
           JR   C,SKIP_M  ;Пропускаем символьную переменную.

           CP   #80
FIND_M4    LD   BC,5
           JR   C,SKIP_BC ;Пропускаем числовую переменную
                          ;с именем из одной буквы.
           CP   #A0
           JR   C,SKIP_M  ;Пропускаем числовой массив.

           CP   #C0
           JR   NC,FIND_M3

;Пропускаем числовую переменную с именем из нескольких символов:

FIND_M2    LD   A,(HL)
           INC  HL
           RLA
           JR   NC,FIND_M2
           JR   FIND_M4

FIND_M3    CP   #E0
           LD   C,18  ;B=0!
           JR   NC,SKIP_BC ;Пропускаем переменную цикла.

;Пропуск символьной переменной или массива:

SKIP_M     LD   C,(HL)
           INC  HL
           LD   B,(HL)
           INC  HL

SKIP_BC    ADD  HL,BC
           JR   FIND_M0

FIND_YES   POP  BC ;Снимаем два теперь не нужных числа со стека.
           POP  BC
           INC  A  ;A станет равным 1, флаг Z сбросится.

FIND_NO    POP  BC
           RET     ;Выход.

Интерпретатор Бейсика производит подобный поиск при каждом обращении к какой-либо переменной — вот вам и одна из причин низкой скорости работы бейсик-программ.

Пример оптимизации

Попробуем оптимизировать по быстродействию какую-либо простую бейсик-программу — например, приведённую ниже.

10 LET N=0: INPUT A$
20 IF A$="" THEN GO TO 60
30 FOR I=1 TO LEN (A$)
40 IF A$(I)="X" THEN LET N=N+1
50 NEXT I
60 PRINT N

Эта программа запрашивает у пользователя строку и подсчитывает, сколько раз в ней встретится символ «X». Критичным по быстродействию участком, очевидно, является цикл в строках 30—50. Перепишем его на ассемблере. Пусть он размещается, скажем, с адреса 40000.

           ORG  40000

           LD   DE,NAME_A ;Ищем переменную A$.
           CALL FIND_VAR

;Теперь HL указывает на 2-байтное значение, определяющее
;длину строки A$.

           LD   E,(HL)
           INC  HL
           LD   D,(HL)
           INC  HL

;DE - длина строки, HL указывает на первый символ строки.

           LD   BC,0     ;Счётчик символов "X".

M1         LD   A,(HL)
           CP   "X"
           JR   NZ,M2
           INC  BC

M2         INC  HL
           DEC  DE
           LD   A,D
           OR   E
           JR   NZ,M1

;BC - количество символов "X" в строке.

           LD   DE,NAME_N ;Ищем переменную N.
           CALL FIND_VAR

;Теперь HL указывает на значение переменной N. Помещаем туда
;содержимое BC, представленное в 5-байтной форме:

           LD   (HL),0
           INC  HL
           LD   (HL),0
           INC  HL
           LD   (HL),C
           INC  HL
           LD   (HL),B
           INC  HL
           LD   (HL),0

           RET  ;Выход.

NAME_A     DB   "a"-#20 ;Имя переменной A$.
           DB   0
NAME_N     DB   "n"     ;Имя переменной N.
           DB   0

<далее следует текст процедуры FIND_VAR>

Результат компиляции запишем в файл «file1». После этого остаётся лишь переписать исходную программу:

5  CLEAR 39999: RANDOMIZE USR 15619: REM: LOAD "file1" CODE
10 LET N=0: INPUT A$
20 IF A$="" THEN GO TO 60
30 RANDOMIZE USR 40000
60 PRINT N

Напомню условия, которым должна удовлетворять вызываемая из Бейсика ассемблерная процедура.

Несколько полезных советов

Значения числовых переменных лучше преобразовать для последующей обработки из 5-байтной формы в одно- или двухбайтную, в зависимости от диапазона: так их можно будет быстрее обрабатывать. Если нужно работать с массивами, и в памяти достаточно свободного места, стоит скопировать их на адреса, кратные 256: это позволит ускорить доступ к элементам такого массива (подробнее о том, как быстро работать с массивами, вы можете прочитать в [4]).

Создание новых переменных

Может быть так, что в переписываемом фрагменте программы выполняется присвоение значения ранее ещё не использовавшейся переменной. В этом случае информация о ней отсутствует в области переменных, и, следовательно, надо её туда добавить. Для этого необходимо выделить в конце области переменных (между концом последней переменной и байтом #80) область памяти необходимой длины и занести в неё имя и значение новой переменной.

Выделение области памяти выполняется с помощью процедуры MAKE_ROOM, расположенной в ПЗУ по адресу #1655. При её вызове в регистровой паре HL должен находиться адрес, с которого нужно выделить место, а в BC — длина выделяемой области.

Адрес выделяемой области памяти нетрудно определить, если учесть, что непосредственно за областью переменных располагается область редактируемых строк программы, адрес начала которой хранится в системной переменной E_LINE. Следовательно, для получения искомого адреса достаточно взять значение E_LINE и уменьшить его на 1.

Рассмотрим пример. Пусть имеется такая программа:

    10 INPUT A: INPUT B
    20 LET C=A+B
    30 PRINT C

и нам нужно переписать на ассемблере строку 20. Известно, что переменные A, B и C — целые, и их значения принадлежат диапазону 0..65535. В этом случае ассемблерный текст будет таким:

           ORG  40000

MAKE_ROOM  EQU  #1655
E_LINE     EQU  23641

           LD   DE,NAME_A
           CALL FIND_VAR

;Теперь HL указывает на значение переменной A,
;DE указывает на NAME_B.

           INC  HL
           INC  HL
           LD   C,(HL)
           INC  HL
           LD   B,(HL)

;BC - значение переменной A.

           CALL FIND_VAR

           INC  HL
           INC  HL
           LD   E,(HL)
           INC  HL
           LD   D,(HL)

;DE - значение переменной B.

           EX   DE,HL
           ADD  HL,BC    ;Вычислили C.
           PUSH HL       ;Запомнили в стеке.

;Выделяем место для переменной C.

           LD   HL,(E_LINE)
           DEC  HL       ;Где выделяем.
           PUSH HL
           LD   BC,6     ;Сколько выделяем.
           CALL MAKE_ROOM
           POP  HL

           LD   (HL),"c" ;Имя переменной.
           INC  HL

           POP  DE       ;Сняли со стека
           LD   (HL),0   ;значение переменной
           INC  HL       ;и записываем его
           LD   (HL),0   ;в 5-байтной
           INC  HL       ;форме.
           LD   (HL),E
           INC  HL
           LD   (HL),D
           INC  HL
           LD   (HL),0

           RET           ;Выход.

NAME_A     DB   "a" ;Имя переменной A.
           DB   0
NAME_B     DB   "b" ;Имя переменной B.
           DB   0

<далее следует текст процедуры FIND_VAR>

Литература

  1. «Бейсик ZX Spectrum». Москва, VA PRINT, 1993.
  2. «Секреты ПЗУ». «ZX-Ревю» 10/1991.
  3. «Персональный компьютер ZX Spectrum. Программирование в машинных кодах и на языке ассемблера». Москва, «Инфорком», 1993.
  4. И.Рощин. «Оптимизация на примере intro „Start“». «Радиомир. Ваш компьютер» 7—10/2001.
Страница Ивана Рощина > Статьи >