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

Архитектура компьютера - Э.Таненбаум

.pdf
Скачиваний:
131
Добавлен:
24.05.2014
Размер:
5.64 Mб
Скачать

Типы команд

403

моебайта,словаит.д.,находящегосявячейкесэтимадресом.Такиекомандынарушают типовую безопасность языкаJava, но они нужны для С и C++.

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

Сравнение наборов команд

Рассмотренные наборы команд очень сильно отличаются друг от друга. Pentium II — это классическая двухадресная 32-битная машина CISC. Она пережила долгую историю, у нее особые и нерегулярные способы адресации, и она содержит множество команд, которые обращаются к памяти. UltraSPARC II — это современная трехадресная 64-битная машина RISC с архитектурой загрузки/сохранения, всего двумя способамиадресации икомпактным и эффективным набором команд.JVM — это машина со стековой организацией, практически без способов адресации, с регулярными командами и очень плотным кодированием команд.

Воснову разработки компьютера Pentium II легли три основных фактора:

1.Обратная совместимость.

2.Обратная совместимость.

3.Обратная совместимость.

При нынешнем положении вещей никто не стал бы разрабатывать такую нерегулярную машину с таким маленьким количеством абсолютно разных регистров. По этой причине очень сложно писать компиляторы. Из-за недостатка регистров компиляторам постоянно приходится сохранять переменные в памяти, а затем вновь загружать их, что очень невыгодно даже при наличии трех уровней кэшпамяти. Только благодаря таланту инженеров компании Intel процессор Pentium II работает достаточно быстро, несмотря на все недостатки уровня команд. Но, как мы увидели в главе 4, реализация этого процессора чрезвычайно сложна и требует транзисторов в два раза больше, чем picojava II, и почти в полтора раза больше, чем UltraSPARC II.

Современная разработка уровня команд представлена в процессоре UltraSPARC II. Он содержит полную 64-битную архитектуру команд (с шиной на 128 битов). Процессор содержит много регистров и имеет набор команд, в котором преобладают трехрегистровые операции, а также имеется небольшая группа команд LOAD и STORE. Все команды одного размера, хотя число форматов вышло из-под контроля. Большинство новых разработок очень похожи на UltraSPARC II, но содержат меньше форматов команд.

JVM — машина совершенно другого рода. Здесь уровень команд изначально разрабатывался так, чтобы небольшие программы можно было передавать по Интернету и интерпретировать на программном обеспечении другого компьютера. Это была разработка для одного языка. Все это привело к использованию стека и коротким командам разной длины с очень высокой плотностью (в среднем всего

4 0 4 Глава 5. Уровень архитектуры команд

1,8 байта на команду). Создание аппаратного обеспечения, которое выполняет одну команду JVM за раз и при выполнении одной команды обращается к памяти два или три раза, кажется нонсенсом. Но благодаря помещению на микросхему стека из 64 слов и переделыванию целых последовательностей команд в современные трехадресные команды RISC машина picojava II умудряется неплохо работать с очень неэффективной архитектурой команд.

Ядро современного компьютера представляет собой сильно конвейеризированное трехрегистровое устройство загрузки/сохранения типа RISC. UltraSPARC II просто открыто сообщает об этой структуре пользователю. Pentium II скрывает эту систему RISC, перенимая старую архитектуру команд и разбивая команды CISC на микрооперации RISC. Машина picojava II также таит в себе ядро RISC, комбинируя несколько команд для получения одной команды RISC.

Поток управления

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

Последовательный поток управления и переходы

Большинство команд не меняют поток управления. После выполнения одной команды вызывается и выполняется та команда, которая идет вслед за ней в памяти. После выполнения каждой команды счетчик команд увеличивается на число, соответствующее длине команды. Счетчик команд представляет собой линейную функцию от времени, которая увеличивается на среднюю длину команды за средний промежуток времени. Иными словами, процессор выполняет команды в том же порядке, в котором они появляются в листинге программы, как показано на рис. 5.24, а.

Если программа содержит переходы, то это простое соотношение между порядком расположения команд в памяти и порядком их выполнения больше не соответствует действительности. При наличии переходов счетчик команд больше не является монотонно возрастающей функцией от времени, как показано на рис. 5.24, б. В результате последовательность выполнения команд из самой программы уже не видна. Если программисты не знают, в какой последовательности процессор будет выполнять команды, это может привести к ошибкам. Такое наблюдение побудило Дейкстру [31] написать статью под названием «Оператор GOTO нужно считать вредным», в котором он предлагал избегать в программах оператора goto. Эта статья дала толчок революции в программировании, одним из нововведений которой было устранение операторов goto более структурированными формами потока управления, например циклами while. Конечно, эти про-

Поток управления

4 0 5

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

Время

Время

 

б

Рис. 5.24. Счетчик команд как функция от времени (приближенно): без переходов (а); с переходами (б)

Процедуры

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

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

Особый интерес представляет рекурсивная процедура. Это такая процедура, которая вызывает сама себя либо непосредственно, либо через цепочку других процедур. Изучение рекурсивных процедур дает значительное понимание того, как реализуются вызовы процедур и что в действительности представляют собой локальные переменные. А теперь рассмотрим пример рекурсивной процедуры.

«Ханойская башня» — это древняя задача, которая имеет простое решение с использованием рекурсии. В одном монастыре в Ханое есть три золотых колышка. Вокруг первого из них располагались 64 концентрических золотых диска, каждый из них с отверстием посередине для колышка. Диаметр дисков уменьшается снизу вверх. Второй и третий колышки абсолютно пусты. Монахи переносят все диски на колышек 3 по одному диску, но диск большего размера не может находиться сверху на диске меньшего размера. Говорят, что когда они закончат, наступит конец света. Если вы хотите потренироваться, вы можете использовать пласти-

4 0 6 Глава 5. Уровень архитектуры команд

ковые диски, и не 64, а поменьше, но когда вы решите эту задачу, ничего страшного не произойдет. Чтобы произошел конец света, требуется 64 диска, и все они должны быть из золота. На рисунке 5.25 показана начальная конфигурация, где число дисков (п) равно 5.

Колышек1

Колышек 2

Колышек 3

 

J

J

Рис.5.25.Исходноеположениевзадаче«Ханойскаябашня»дляпятидисков

Чтобы переместить п дисков с колышка 1 на колышек 3, нужно сначала перенести п-1 дисков с колышка 1 на колышек 2, затем перенести один диск с колышка1 на колышек 3, а потом перенести п-1 диск с колышка 2 на колышек 3. Решение этой задачи проиллюстрировано на рис. 5.26.

Для решения задачи нам нужна процедура, которая перемещает п дисков с колышка i на колышек]. Когда эта процедура вызывается,

towers (n I j)

решение выводится на экран. Сначала процедура проверяет, равно ли п единице. Если да, то решение тривиально: нужно просто переместить один диск с i наj. Если п не равно 1, решение состоит из трех частей, как было сказано выше, и каждая из этих частей представляет собой рекурсивную процедуру.

Полное решение показано в листинге 5.6. Вызов процедуры towers (3. 1 3)

порождает еще три вызова

towers (2 1 2) towers (I 1.3) towers (2, 2 3)

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

Листинг5.6.Процедурадлярешениязадачи«Ханойскаябашня»

public void towers (int n. int l. int j) int k.

if (n==l)

System out рппШС'Переместить диск из" + i + "на" + j ) . else k=6-i-j.

towers(n-l. l, k) towers (1 . i. j ) . towers (n-1. k. j ) .

Потокуправления

4 0 7

Первоначальное

состояние

Сначала

перемещаем два диска с колышка 1 на колышек 2

Затем перемещаем один диск

с колышка 1 на колышек 3

Наконец,

перемещаем два диска с колышка 2 на колышек 3

Рис. 5.26. Решение задачи «Ханойская башня» для трехдисков

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

Помимо указателя стека, который указывает на вершину стека, удобно иметь указатель фрейма (FP — Frame Pointer), который указывает на фиксированное

4 0 8 Глава 5. Уровень архитектуры команд

место во фрейме. Он может указывать на связующий указатель, как в IJVM, или на первую локальную переменную. На рис. 5.27 изображен стековый фрейм для машины с 32-битным словом. При первом вызове процедуры towers в стек помещаются n, i и j, а затем выполняется команда CALL, которая помещает в стек адрес возврата, 1012. Вызванная процедура сохраняет в стеке старое значение FP (1000) в ячейке 1016, а затем передвигает указатель стека для обозначения места хранения локальных переменных. При наличии только одной 32-битной локальной переменной (k) SP (Stack Pointer — указатель стека) увеличивается на 4 до 1020. На рис. 5.27, а показан результат всех этих действий.

Первое, что должна сделать процедура после того, как ее вызвали, — это сохранить предыдущее значение FP (так, чтобы его можно было восстановить при выходе из процедуры), скопировать значение SP в FP и, возможно, увеличить на одно слово, в зависимости от того, куда указывает FP нового фрейма. В этом примере FP указывает на первую локальную переменную, а в IJVM LV указывает на связующий указатель. Разные машины оперируют с указателем фрейма немного поразному, иногда помещая его в самый низ стекового фрейма, иногда — в вершину,

а иногда — в середину, как на рис. 5.27. В этом отношении стоит сравнить рис. 5.27

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

Код, который сохраняет старый указатель фрейма, устанавливает новый указатель фрейма и увеличивает указатель стека, чтобы зарезервировать пространство для локальных переменных, называется прологом процедуры. При выходе из процедуры стек должен быть очищен, и этот процесс называется эпилогом процедуры. Одна из важнейших характеристик компьютера — насколько быстро он может совершать пролог и эпилог. Если они очень длинные и выполняются медленно, делать вызовы процедур будет невыгодно. Команды ENTER и LEAVE в машине Pentium II были разработаны для того, чтобы пролог и эпилог процедуры работали эффективно. Конечно, они содержат определенную модель обращения с указателем фрейма, и если компилятор имеет другую модель, их нельзя использовать.

А теперь вернемся к задаче «Ханойская башня». Каждый вызов процедуры добавляет новый фрейм к стеку, а каждый выход из процедуры удаляет фрейм из стека. Ниже мы проиллюстрируем, как используется стек при реализации рекурсивных процедур. Начнем с вызова

towers (3. 1, 3)

На рис. 5.27, а показано состояние стека сразу после вызова процедуры. Сначала процедурапроверяет, равнолипединице, аустановив, что п=3,заполняеткисовершает вызов

towers (2. 1. 2)

Состояние стека после завершения этого вызова показано на рис. 5.27, б. После этого процедура начинается с начала (вызванная процедура всегда начинается с начала). На этот раз условие п=1 снова не подтверждается, поэтому процедура снова заполняет к и совершает вызов

towers (1. 1, 3)

 

 

 

 

Поток управления

409

 

 

 

 

 

 

 

Адрес

 

SP

-->

k

SP

-->

k=3

1068

 

 

 

Старое

 

 

Старое

 

 

 

Г

-значение FP

 

 

-значение FP

1064

 

 

 

=1024

 

 

=1024

 

 

 

 

Адрес

 

 

Адрес

1060

 

 

 

возврата

 

 

возврата

 

 

 

 

 

 

 

 

 

J=3

 

 

j=2

1056

 

 

 

i=1

 

 

i=1

1052

 

FP

- >

n=1

FP

- >

n=1

1048

SP -->

k

 

k=3

k=3

 

k=3

1044

 

 

Адрес

 

 

возврата

 

 

Старое

 

 

значение FP

 

 

=1000

 

 

j=2

 

 

i=1

 

FP " >

n=2

S P - ^

k

k=2

 

Старое

Старое

 

значение FP

значение FP

 

Адрес

Адрес

 

возврата

возврата

 

j=3

j=3

 

i=1

i=1

FP-K

n=3

n=3

 

а

б

>

Старое

Старое

Старое

 

значение FP

-значение FP

значение FP

1040

 

=1000

=1000

=1000

 

 

Адрес

Адрес

Адрес

1036

 

возврата

возврата

возврата

 

j=2

j=2

j=2

1032

 

i=1

i=1

i=1

1028

 

n=2

n=2

n=2

1024

 

k=2

k=2

k=2

1020

 

 

 

 

 

Старое

Старое

Старое

1016

 

значение FP

значение FP

значение FP

 

 

 

Адрес

Адрес

Адрес

1012

 

возврата

возврата

возврата

 

 

 

J=3

j=3

j=3

1008

 

i=1

i=1

i=1

1004

 

n=3

n=3

n=3

1000

 

в

г

д

 

Рис.5.27.Состояниестекавовремявыполненияпрограммылистинга5.6

4 1 0 Глава 5. Уровень архитектуры команд

Состояние стека после этого вызова показано на рис. 5.27, в. Счетчик команд указывает на начало процедуры. На этот раз условие подтверждается, и на экран выводится строка. Затем совершается выход из процедуры. Для этого удаляется один фрейм, а значения FP и SP переопределяются (см. рис. 5.27, г). Затем процедурапродолжаетвыполнятьсявадресевозврата:

towers (1. 1. 2)

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

towers (3. 1. 3)

используяручкуибумагу.

Сопрограммы

В обычной последовательности вызовов существует четкое различие между вызывающей процедурой и вызываемой процедурой. Рассмотрим процедуру А, которая вызывает процедуру В (рис. 5.28).

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

Различие состоит в том, что когда управление переходит от А к В, процедура В начинает выполняться с самого начала; а при переходе из В обратно в А выполнение начинается не с начала процедуры А, а с того момента, за которым последовал вызов процедуры В. Если А работает некоторое время, а потом снова вызывает процедуру В, выполнение В снова начинается с самого начала, а не с того места, после которого произошло возвращение к процедуре А. Если процедура А вызывает процедуру В много раз, процедура В каждый раз начинается с начала, а процедура А уже никогда больше с начала не начинается.

Это различие отражается в способе передачи управления между А и В. Когда А вызывает В, она использует команду вызова процедуры, которая помещает адрес возврата (то есть адрес того выражения, которое последует за процедурой) в такое место, откуда его потом легко будет вытащить, например в вершину стека. Затем она помещает адрес процедуры В в счетчик команд, чтобы завершить вызов. Для выхода из процедуры В используется не команда вызова процедуры, а команда выхода из процедуры, которая просто выталкивает адрес возврата из стека и помещает его в счетчик команд.

Иногда нужно иметь две процедуры А и В, каждая из которых вызывает другую в качестве процедуры, как показано на рис. 5.29. При возврате из В к А про-

Поток управления

4 1 1

цедура В совершает переход к тому оператору, за которым последовал вызов процедуры В. Когда процедура А передает управление процедуре В, она возвращается не к самому началу В (за исключением первого раза), а к тому месту, на котором произошел предыдущий вызов А. Две процедуры, работающие подобным образом, называются сопрограммами.

Вызывающая

Вызываемая

процедура

процедура

Процедура А вызывается из основной программы

Процедура А возвращается *

в основную программу

Рис. 5.28. Выполнениевызванной процедуры всегда начинается с самого начала этой процедуры

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

412

Глава 5. Уровеньархитектуры команд

Процедура А вызывается из основной программы

Процедура А возвращается в основную программу

Рис. 5.29. После завершения сопрограммы выполнение начинается с того места, на котором оно завершилось в прошлый раз, а не с самого начала

Обычные команды CALL и RETURN не подходят для вызова сопрограмм, поскольку адрес для перехода берется из стека, как и при возврате, но, в отличие от возврата, при вызове сопрограммы адрес возврата помещается в определенном месте, чтобы в последующем к нему вернуться. Было бы неплохо, если бы существовала команда для замены вершины стека на счетчик команд. Эта команда сначала выталкивала бы старый адрес возврата из стека и помещала бы его во внутренний регистр, затем помещала бы счетчик команд в стек и, наконец, копировала бы содержание внутреннего регистра в счетчик команд. Поскольку одно слово выталкивается из стека, а другое помещается в стек, состояние указателя стека не меняется. Такая команда встречается очень редко, поэтому в большинстве случаев ее приходится моделировать из нескольких команд.

Ловушки

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