
Boroda_2
.docДоступ к внешней памяти данных осуществляется по-другому. В 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 начали осознавать, что не слишком хорошо иметь оператор цикла с такой странной семантикой, хотя она и позволяет экономить одну команду перехода на каждом цикле.