Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Boroda_2

.doc
Скачиваний:
9
Добавлен:
20.03.2015
Размер:
1.67 Mб
Скачать

Доступ к внешней памяти данных осуществляется по-другому. В 8051 есть 16-разрядный регистр с двойным указателем (DPTR), в котором размещаются 16-разрядные адреса памяти. Программы загружают этот регистр и через него обращаются к любым байтам из 64 Кбайт памяти.

Сравнение режимов адресации

Рассмотренные режимы адресации машин Pentium 4, UltraSPARC III и 8051 ил­люстрирует табл. 5.8. Как мы уже отмечали, в командах может использоваться не каждый режим.

Таблица 5.8. Сравнение режимов адресации

Режим адресации

Pentium 4

UltraSPARC III

8051

Неявная

Да

Непосредственная

Да

Да

Да

Прямая

Да

Да

Регистровая

Да

Да

Да

Косвенная регистровая

Да

Да

Да

Индексная

Да

Да

Относительная индексная

Да

Стековая

На практике для эффективной архитектуры команд вовсе не нужно множест­во разнообразных режимов адресации. Поскольку практически весь код, напи­санный на этом уровне, порождается компиляторами (за исключением 8051), ре­жимов адресации должно быть мало и они должны быть простыми и понятными. Машине приходится занимать одну из крайних позиций, предлагая либо все дос­тупные варианты, либо только один вариант. В промежуточных случаях компи­лятор сталкивается с необходимостью выбора, сделать который он не в силах ввиду недостатка информации или сложности алгоритма.

Поэтому в наиболее простых архитектурах используют очень немного режи­мов адресации, причем на каждый из этих режимов накладываются жесткие ограничения. Обычно практически для любых применений достаточно непо­средственной, прямой, регистровой и индексной адресации. Каждый регистр (включая указатель локальных переменных, указатель стека и счетчик команд) должен быть доступен всегда, когда это требуется. В более сложных режимах ад­ресации можно сократить число команд, но при этом придется ввести жесткие последовательности операций, которые трудно будет выполнять параллельно с другими операциями.

Мы рассмотрели возможные компромиссы между кодами операций и адре­сами, а также между различными режимами адресации. Когда вы сталкиваетесь с новым компьютером, нужно изучить все его команды и режимы адресации, причем не только для того, чтобы знать об их существовании, но и чтобы понять, почему разработчиками был сделан именно такой выбор и каковы были бы по­следствия при другом выборе.

Типы команд

Команды можно грубо поделить на несколько групп, которые, хотя и могут раз­личаться в деталях, воспроизводятся практически в каждой машине. Кроме того, в каждом компьютере всегда имеется несколько необычных команд, добавлен­ных либо в целях совместимости с предыдущими моделями, либо потому, что у разработчика возникла блестящая идея, либо по требованию правительства, за­платившего производителю, чтобы тот включил в набор команд новую команду. В этом разделе мы попытаемся описать все наиболее распространенные катего­рии, однако отметим, что мы не претендуем на исчерпывающее изложение.

Команды перемещения данных

Копирование данных из одного места в другое — одна из самых распространен­ных операций. Под копированием мы понимаем создание нового объекта с точно таким же набором битов, как у исходного. Такое понимание слова «перемеще­ние» несколько отличается от его обычного значения. Если мы говорим, что ка­кой-то человек переместился из Нью-Йорка в Калифорнию, это не значит, что в Калифорнии была создана идентичная копия этого человека, а оригинал остал­ся в Нью-Йорке. Когда мы говорим, что содержимое ячейки памяти 2000 пере­местилось в какой-либо регистр, мы всегда подразумеваем, что там была создана идентичная копия, а оригинал все еще находится в ячейке 2000. Команды пере­мещения данных лучше было бы назвать командами дублирования данных, но название уже устоялось.

Есть две причины, по которым данные могут копироваться из одного места в другое. Одна из них фундаментальна: присваивание переменным значений. Следующая операция присваивания выполняется путем копирования значения, которое находится в ячейке памяти с адресом В, в ячейку А, поскольку програм­мист дал команду это сделать:

А = В

Вторая причина копирования данных — предоставить возможность быстрого обращения к ним. Как мы уже видели, многие команды могут обращаться к пе­ременным только в том случае, если они находятся в регистре. Поскольку суще­ствует два возможных источника данных (память и регистр) и два возможных приемника данных (память и регистр), существует также 4 различных способа копирования. В одних компьютерах для этих четырех случаев поддерживаются 4 команды, в других — единственная команда. Некоторые компьютеры использу­ют команду LOAD для загрузки данных из памяти в регистр, команду STORE для со­хранения в памяти данных из регистра, команду MOVE для перемещения данных из одного регистра в другой, но вообще не имеют команд для копирования из од­ной части памяти в другую.

Команды перемещения данных должны как-то указывать, сколько данных нужно переместить. Существуют команды для перемещения разных объемов данных — от одного бита до всей памяти. В машинах с фиксированной длиной слова обычно перемещается ровно одно слово. Любые перемещения других объ­емов данных (больше или меньше слова) должны выполняться программно с ис­пользованием операций сдвига и слияния. Некоторые архитектуры команд дают возможность копировать фрагменты данных размером меньше слова (они обычно измеряются в байтах), а также сразу нескольких слов. Копирование нескольких слов рискованно, особенно если максимальное количество слов достаточно боль­шое, поскольку такая операция может занять много времени и существует веро­ятность ее прерывания в середине. Некоторые машины с переменной длиной слов содержат команды, которые определяют только адреса источника и приемника, но не объем данных — в этом случае перемещение продолжается до тех пор, пока не встретится специальное поле, идентифицирующее окончание данных.

Бинарные операции

Бинарные операции — это такие операции, которые получают результат на основе значений двух операндов. Все архитектуры команд содержат команды для сло­жения и вычитания целых чисел. Кроме того, практически во всех архитектурах имеются команды умножения и деления целых чисел. Вероятно, нет необходи­мости объяснять, почему компьютеры оснащены арифметическими командами.

В следующую группу бинарных операций входят булевы команды. Хотя су­ществует 16 булевых функций от двух переменных, команды для всех 16 поддер­живаются в очень немногих машинах. Обычно поддерживаются только опера­ции И, ИЛИ и НЕ; иногда кроме них еще ИСКЛЮЧАЮЩЕЕ ИЛИ, НЕ-ИЛИ и НЕ-И.

Важным применением операции И является выделение битов из слов. Рас­смотрим машину со словами длиной в 32 бита, в которой на одно слово прихо­дится четыре 8-разрядных символа. Предположим, что нужно отделить второй символ от остальных трех, чтобы его напечатать. Это значит, что нужно создать слово, в котором этот символ займет правые 8 бит, а левые 24 бит должны стать нулевыми (так называемое выравнивание вправо).

Чтобы извлечь нужный нам символ, слово, содержащее этот символ, соединя­ется операцией И с константой, которая называется маской. В результате этой операции все ненужные биты меняются на нули:

А:

10110111 10111100 11011011 10001011

В (маска):

00000000 11111111 00000000 00000000 АН В:

00000000 10111100 00000000 00000000

Затем результат сдвигается на 16 бит вправо, чтобы нужный символ оказался в правой части слова.

Важным применением команды ИЛИ является помещение битов в слово. Эта операция обратна операции извлечения. Чтобы изменить правые 8 бит 32-раз- рядного слова, не повредив при этом остальные 24 бита, сначала нежелательные 8 бит надо заменить нулями, а затем новый символ соединить операцией ИЛИ с полученным результатом:

А:

10110111 10111100 11011011 10001011 В (маска):

11111111 11111111 11111111 00000000 А И В:

10110111 10111100 11011011 00000000

С:

00000000 00000000 00000000 01010111 И В) ИЛИ С:

10110111 10111100 11011011 01010111

Операция И удаляет единицы, поэтому в полученном результате никогда не бы­вает больше единиц, чем в любом из двух операндов. Операция ИЛИ вставляет еди­ницы, поэтому в полученном результате всегда по крайней мере столько же единиц, сколько в операнде с большим количеством единиц. Операция ИСКЛЮЧАЮЩЕЕ ИЛИ, в отличие от них, симметрична в отношении единиц и нулей. Такая симмет­рия иногда может быть полезной, например при порождении случайных чисел.

Большинство компьютеров сегодня поддерживают команды с плавающей точкой, которые в основном соответствуют арифметическим операциям с целыми числами. Большая часть машин поддерживает по крайней мере 2 варианта таких чисел: более короткие для скорости и более длинные на тот случай, если требуется высокая точность вычислений. Существует множество возможных форматов для чисел с плавающей точкой, но сейчас практически везде применяется единый стандарт IEEE 754. Числа с плавающей точкой и этот стандарт обсуждаются в приложении Б.

Унарные операции

В унарных операциях результат получается обработкой единственного операнда. Поскольку в данном случае нужно задавать на один адрес меньше, чем в бинар­ных операциях, команды иногда бывают короче, если только не требуется зада­вать другую информацию.

Очень полезны команды сдвига и циклического сдвига. Они часто даются в нескольких вариантах. Сдвиги — это операции, при которых биты сдвигаются влево или вправо, при этом биты, которые сдвигаются за пределы слова, утрачи­ваются. Циклические сдвиги — это сдвиги, при которых биты, вытесненные с од­ного конца, появляются на другом конце слова. Разницу между обычным сдви­гом и циклическим сдвигом иллюстрирует следующий пример:

А:

00000000 00000000 00000000 01110011 Сдвиг А вправо на 2 бита:

00000000 00000000 00000000 00011100 Циклический сдвиг А вправо на 2 бита:

11000000 00000000 00000000 00011100

Обычные и циклические сдвиги влево и вправо играют очень важную роль. Если гг-разрядное слово циклически сдвигается влево на k бит, результат будет такой же, как при циклическом сдвиге вправо на п - k бит.

Сдвиги вправо часто выполняются с расширением по знаку. Это значит, что позиции, освободившиеся на левом конце слова, заполняются знаковым битом (О или 1) исходного слова, как будто знаковый бит перетаскивают вправо. Кроме того, это значит, что отрицательное число остается отрицательным. Вот как вы­глядят сдвиги на 2 бита вправо:

Л\

1111111 11111111 11111111 11110000

А сдвигается без знакового расширения:

0011111 11111111 11111111 11111100

А сдвигается со знаковым расширением:

1111111 11111111 11111111 11111100

Операция сдвига используется при умножении и делении на 2. Если положи­тельное целое число сдвигается влево на k бит, результатом будет исходное чис­ло, умноженное на 2к. Если положительное целое число сдвигается вправо на k бит, результатом становится исходное число, деленное на 2к.

Сдвиги могут использоваться для повышения скорости выполнения некоторых арифметических операций. Рассмотрим выражение 18 х гг, где п — положительное целое число: 18 х гг = 16хгг + 2хгг. Значение 16 х гг можно получить путем сдвига копии гг на 4 бита влево. Значение 2 х гг можно получить, сдвинув гг на 1 бит влево. Сумма этих двух чисел равна 18 х гг. Таким образом, целиком произведение можно вычислить путем одного перемещения, двух сдвигов и одного сложения, что обычно выполняется гораздо быстрее, чем операция умножения. Конечно, компилятор мо­жет применить такую схему, только если один из множителей является константой.

Сдвиг отрицательных чисел даже со знаковым расширением дает совершенно другие результаты. Рассмотрим, например, число -1 в обратном двоичном коде. При сдвиге влево на 1 бит получается число -3. При сдвиге влево еще на 1 бит получается число -7:

Число -1 в обратном двоичном коде:

11111111 11111111 11111111 11111110

Число -1 сдвигается влево на 1 бит (-3):

11111111 11111111 11111111 11111100

Число -1 сдвигается влево на 2 бита (-7):

11111111 11111111 11111111 11111000

Сдвиг влево отрицательных чисел в обратном двоичном коде не ведет к умно­жению числа на 2. В то же время сдвиг вправо корректно обеспечивает деление.

А теперь рассмотрим число -1 в дополнительном двоичном коде. При сдвиге вправо на 6 бит с расширением по знаку получается число -1, что неверно, по­скольку целая часть от -1/64 равна 0:

Число -1 в дополнительном двоичном коде:

11111111 11111111 11111111 11111111

Число -1, сдвинутое влево на 6 бит, равно -1:

11111111 11111111 11111111 11111111

Как правило, сдвиг вправо вызывает ошибки, поскольку он производит ок­ругление до большего отрицательного целого числа, что недопустимо при вы­полнении целочисленных арифметических операций с отрицательными числа­ми. В то же время сдвиг влево аналогичен умножению на 2.

Операции циклического сдвига нужны для манипулирования последователь­ностями битов в словах. Чтобы проверить все биты в слове, каждый бит путем циклического сдвига слова поочередно помещается в знаковый разряд, где его можно легко проверить; когда все биты будут проверены, восстанавливается изначальное значение слова. Операции циклического сдвига гораздо удобнее операций обычного сдвига, поскольку при этом не теряется информация: произ­вольная операция циклического сдвига может быть отменена операцией цикли­ческого сдвига в противоположную сторону.

В некоторых бинарных операциях обычно используются совершенно опреде­ленные операнды, поэтому для быстрого выполнения аналогичных действий в архитектуры команд часто включаются унарные операции. Например, в начале вычислений чрезвычайно часто требуется перемещение нуля в память или в ре­гистр. Перемещение нуля — это особый случай команды перемещения данных. Поэтому для повышения производительности в архитектуру вводится операция CLR с единственным операндом — адресом той ячейки, которую нужно очистить (то есть установить на 0).

Прибавление 1 к слову тоже часто требуется при различных подсчетах. Унар­ная форма команды ADD — это операция инкремента INC. Другой пример — опера­ция NEG. Отрицание X — это на самом деле бинарная операция вычитания 0 - X, но поскольку операция отрицания применяется очень часто, в архитектуру ко­манд вводится команда NEG. Важно понимать разницу между арифметической операцией NEG и логической операцией NOT. При выполнении операции NEG про­исходит аддитивная инверсия числа (такое число, сумма которого с исходным числом дает 0). При выполнении операции NOT все биты в слове просто инверти­руются. Эти операции очень похожи, а для системы, в которой отрицательные величины представлены в обратном двоичном коде, они идентичны. (В арифме­тике дополнительных кодов для выполнения команды NEG сначала инвертируют­ся все биты, а затем к полученному результату прибавляется 1.)

Унарные и бинарные операции часто объединяются в группы по функциям, которые они выполняют, а вовсе не по числу операндов. В первую группу входят арифметические операции, в том числе операция отрицания. Во вторую группу входят логические операции и операции сдвига, поскольку эти две категории очень часто используются вместе для извлечения данных.

Сравнения и условные переходы

Практически все программы должны проверять свои данные и на основе полу­ченных результатов изменять последовательность выполняемых команд. Рассмот­рим функцию извлечения квадратного корня из числа х. Если это число отрица­тельно, функция должна сообщать об ошибке, если положительно — выполнять вычисления. То есть соответствующая программа должна проверять знак чис­ла х, а затем совершать переход в зависимости от полученного результата.

Этот алгоритм можно реализовать с помощью специальных команд условно­го перехода, проверяющих разного рода условия и при их выполнении совер­шающих переходы к определенным адресам памяти. Иногда один из битов в ко­манде указывает, нужно ли осуществлять переход в случае выполнения или в случае не выполнения условия. Часто целевой адрес является не абсолютным, а относительным (связанным с текущей командой).

Самое распространенное условие — проверка на равенство или на неравенст­во определенного бита нулю. Если команда проверяет знаковый бит числа и со­вершает переход к метке (LABEL), когда этот бит равен 1 (проверяемое число от­рицательно), то выполняются те команды, которые начинаются с метки LABEL, а если этот бит равен 0 (проверяемое число положительно), то выполняются ко­манды, следующие за командой условного перехода.

Во многих машинах имеются биты кодов условий; например, это может быть бит переполнения, который принимает значение 1 всякий раз, когда арифме­тическая операция выдает неправильный результат. По этому биту проверяется правильность выполнения предыдущей арифметической операции и в случае ошибки запускается программа обработки ошибок.

В некоторых процессорах есть специальный бит переноса, который принима­ет значение 1, если происходит перенос из самого левого бита (например, при сложении двух отрицательных чисел). Бит переноса нельзя путать с битом пере­полнения. Проверка бита переноса необходима для вычислений с повышенной точностью (то есть когда целое число представлено двумя или более словами).

Проверка на ноль очень важна при выполнении циклов и в некоторых других случаях. Если бы все команды условного перехода проверяли только 1 бит, то тогда для проверки определенного слова на равенство 0 нужно было бы поочередно про­верять каждый бит, чтобы убедиться, что ни один из них не равен 1. Чтобы избе­жать подобной ситуации, многие машины поддерживают команду, которая прове­ряет слово целиком и выполняет переход, если оно равно 0. Конечно же, в этом решении все равно проверяется каждый бит, просто ответственность за проверку перекладывается на микроархитектуру. На практике в число устройств обычно включается регистр, все биты которого соединяются операцией ИЛИ, чтобы полу­чить в результате бит, показывающий, имеются ли в регистре единичные биты. Бит Z на рис. 4.1 обычно вычисляется следующим образом: сначала все выходные биты АЛУ соединяются операцией ИЛИ, а затем полученный результат инвертируется.

Операция сравнения слов или символов очень важна, например, при сорти­ровке. Чтобы выполнить сравнение, требуется три адреса: два нужны для дан­ных, а по третьему адресу будет совершаться переход в случае выполнения усло­вия. В тех компьютерах, где форматы команд позволяют иметь три адреса в команде, проблем не возникает. Но если такие форматы не предусмотрены, нуж­но что-то делать, чтобы обойти проблему.

Одно из возможных решений — ввести команду, которая выполняет сравне­ние и записывает результат в виде одного или нескольких битов условий. Сле­дующая команда может проверять биты условия и совершать переход, если два сравниваемых значения были равны, или если они были неравны, или если пер­вое из них было больше второго и т. д. Такой подход применяется в Pentium 4 и UltraSPARC III.

В операции сравнения двух чисел есть некоторые тонкости. Сравнение — это не такая простая операция, как вычитание. Если очень большое положительное число сравнивается с очень большим отрицательным числом, операция вычита­ния приведет к переполнению, поскольку результат вычитания будет невозмож­но представить. Тем не менее команда сравнения и в этом случае должна опре­делить, удовлетворяется ли условие, и возвратить правильный ответ. При сравнении переполнения возникать не должно.

Кроме того, при сравнении чисел нужно решить, считать ли числа числами со знаком или числами без знака. 3-разрядные бинарные числа можно упорядочить двумя способами. От самого маленького к самому большому:

  • Без знака: ООО, 001, 010, 011, 100, 101, 110, 111.

  • Со знаком: 100, 101, 110, 111, 000, 001, 010, 011.

В первой строке по возрастанию перечислены положительные числа от 0 до 7, во второй — целые числа со знаком от -4 до +3 в дополнительном двоичном коде (тоже по возрастанию). Ответ на вопрос: «Какое число больше: 011 или 100?» — зависит от того, считаются ли числа числами со знаком. В большинстве архитек­тур есть команды для обработки обоих вариантов упорядочения.

Команды вызова процедур

Процедурой называют группу команд, которая решает определенную задачу и которую можно вызывать из разных мест программы. Вместо термина «про­цедура» часто используется термин «подпрограмма», особенно когда речь идет о программах на языке ассемблера. Когда процедура заканчивает решение задачи, она должна вернуться к оператору, расположенному в программе следом за опе­ратором вызова процедуры. Следовательно, адрес возврата должен как-то пере­даваться процедуре или сохраняться где-либо таким образом, чтобы можно было определить, куда возвращаться после решения задачи.

Адрес возврата может помещаться в одном из трех мест: в памяти, в регистре или в стеке. Самое худшее решение — поместить этот адрес в фиксированную ячейку памяти. Тогда, если процедура вызовет другую процедуру, второй вызов приведет к потере первого адреса возврата.

Более удачное решение — сохранить адрес возврата в первом слове процеду­ры, а первой выполняемой командой сделать второе слово процедуры. После за­вершения процедуры будет происходить переход к первому слову, а если аппа­ратно в первом слове наряду с адресом возврата предоставить код операции, произойдет непосредственный переход к этой операции. Процедура может вызы­вать другие процедуры, поскольку в каждой процедуре имеется пространство для одного адреса возврата. Но если процедура вызывает сама себя, эта схема не сработает, поскольку первый адрес возврата будет уничтожен вторым вызовом. (Способность процедуры вызывать саму себя, называемая рекурсией, очень важ­на и теоретически, и практически.) Более того, если процедура А вызывает про­цедуру В, процедура В вызывает процедуру С, а процедура С вызывает процеду­ру А (непосредственная, или цепочечная, рекурсия), эта схема сохранения адреса возврата также не сработает.

Еще более удачное решение — поместить адрес возврата в регистр. Тогда если процедура рекурсивна, ей придется помещать адрес возврата в другое место каж­дый раз, когда она вызывается.

Самое лучшее решение — поместить адрес возврата в стек. Тогда при завер­шении процедуры, она должны выталкивать адрес возврата из стека. При такой форме вызова процедур рекурсия не порождает никаких проблем; адрес возврата будет автоматически сохраняться таким образом, чтобы не уничтожить предыду­щий адрес возврата. Мы рассматривали такой способ сохранения адреса возвра­та в машине IJVM (см. рис. 4.10).

Управление циклами

Часто возникает необходимость выполнять некоторую группу команд фиксиро­ванное количество раз, поэтому некоторые машины для управления этим про­цессом содержат специальные команды. Во всех схемах подобного рода имеется счетчик, который увеличивается или уменьшается на какую-либо константу ка­ждый раз при выполнении цикла. Кроме того, этот счетчик каждый раз проверя­ется. В случае удовлетворения проверяемого условия цикл завершается.

Счетчик запускается вне цикла, и затем сразу начинается выполнение цикла. Последняя команда цикла обновляет счетчик, и, если условие завершения цикла еще не удовлетворено, происходит возврат к первой команде цикла. Если усло­вие удовлетворено, цикл завершается и начинается выполнение команды, распо­ложенной сразу после цикла. Цикл такого типа с проверкой в конце цикла пред­ставлен в листинге 5.3. (Мы не могли здесь использовать язык Java, поскольку в нем нет оператора goto).

Листинг 5.3. Цикл с проверкой в конце

i = 1;

L1: первый оператор;

последний оператор; i = i + 1; if (i<n) goto LI;

Цикл такого типа всегда будет выполнен хотя бы один раз, даже если п < 0. Представим программу, которая поддерживает базу данных о персонале компа­нии. В определенном месте программа начинает считывать информацию о кон­кретном работнике. Она считывает число п — количество детей у работника, и выполняет цикл п раз, по одному разу на каждого ребенка. В цикле она считы­вает его имя, пол и дату рождения, так что компания сможет послать ему (или ей) подарок точно в срок. Однако если у работника нет детей и значение п равно 0, цикл все равно будет выполнен один раз, что даст ошибочные результаты.

В листинге 5.4 представлен другой способ проверки, который дает правиль­ные результаты даже при п < 0. Отметим, что если и увеличение счетчика, и про­верка условия выполняются в одной команде, разработчикам придется выбирать один из двух методов.

i = 1;

LI: if(i>n) goto L2;

первый оператор;

последний оператор;

1=1+1; goto L1;

L2:

Рассмотрим код, который породит компилятор при обработке следующей строки:

for (1=0; 1<n; 1++) { операторы }

Если у компилятора нет никакой информации о числе п, ему, чтобы коррект­но обработать случай п < 0, придется действовать так, как показано в листин­ге 5.4. Однако если компилятор определит, что п > 0 (например, проверив, какое значение присвоено п), он сможет использовать более эффективный код, пред­ставленный в листинге 5.3. Когда-то в стандарте языка FORTRAN требовалось, чтобы все циклы выполнялись хотя бы один раз. Это позволяло всегда порож­дать более эффективный код (как в листинге 5.3). В 1977 году этот дефект был исправлен, поскольку даже приверженцы языка FORTRAN начали осознавать, что не слишком хорошо иметь оператор цикла с такой странной семантикой, хо­тя она и позволяет экономить одну команду перехода на каждом цикле.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]