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

Boroda_2

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

А теперь перейдем к команде 4, которая должна использовать регистр R4. К сожалению, из таблицы мы видим, что в регистр R4 в данный момент произво­дится запись (см. строку 3 в таблице). Здесь имеет место RAW-взаимозависи- мость, поэтому блок декодирования простаивает до тех пор, пока регистр R4 не станет доступным. Во время простоя блок декодирования прекращает получать команды из блока выборки команд. Когда внутренние буферы блока выборки команд заполнятся, он прекращает вызывать команды из памяти.

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

Посмотрим, что происходит в цикле 3. Команда 2, а это команда сложения (два цикла), завершается в конце цикла 3. Но ее результат не может быть сохранен в регистре R4 (который тогда освободится для команды 4). Почему? Из-за необ­ходимости записи результатов в регистры в соответствии с порядком выполне­ния программы. Но зачем? Что плохого произойдет, если сохранить результат в регистре R4 сейчас и сделать это значение доступным?

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

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

В цикле 6 команда 6 простаивает, потому что ей нужно записать результат в регистр R1, а регистр R1 занят. Выполнение команды начинается только в цик­ле 9. Чтобы завершить всю последовательность из 8 команд, требуется 15 циклов из-за многочисленных ситуаций взаимозависимости, хотя аппаратура способна выдавать по две команды за цикл. По колонкам «Выдача» и «Завершение» табл. 4.11 видно, что все команды выдаются из блока декодирования по порядку и завершаются эти команды тоже по порядку.

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

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

Теперь посмотрим на команды 6, 7 и 8 в табл. 4.11. Здесь мы видим, что ко­манда 6 помещает вычисленное значение в регистр R1 и это значение использу­ется командой 7.

Цикл # Команда Вы- Завер- Считываемые регистры Записываемые регистры

дача шение 0123456701234567

1

1

R3 = R0 х R1

1

1

1

1

1 1

2

R4 = RO + R2

2

2

1

1

2

3

R5 = RO + R1

3

3

2

1

1 1 1

4

R6 = R1 + R4

-

3

2

1

1 1 1

3

5

R7 = R1 х R2

5

2

3

3

2

111 1

6

S1 = RO - R2

6

4

3

3

111 1

3

3

2

1 1 1

4

7

R3 = R3 х S1

4

1

3

4

2

1

1 111

8

S2 = R4 + R4

-

3

3

4

2

1

1 111

8

3

4

2

3

1 111

2

3

2

3

1 1 1

1

2

2

3

1 1

5

6

2

1

3

1

1 1

6

7

4

2

1

3

1

1 1 1

5

1

1

2

1

1 1

8

2

1

1

1

7

1

8

1

9

7

Мы также видим, что это значение больше не требуется, потому что команда 8 переписывает значение регистра R1. Нет никакой надобности использовать ре­гистр R1 для хранения результата команды 6. Еще хуже то, что далеко не луч­шим является выбор регистра R1 в качестве промежуточного, хотя, с точки зре­ния программиста, привыкшего к идее последовательного выполнения команд без перекрытий, этот выбор является самым разумным.

В табл. 4.12 мы ввели новый метод для решения этой проблемы — подмену регистров (register renaming). Блок декодирования заменяет регистр R1 в коман­дах 6 (цикл 3) и 7 (цикл 4) скрытым для программиста регистром S1. После это­го команда 6 может запускаться одновременно с командой 5. Современные про­цессоры содержат десятки скрытых регистров, которые используются для подмены. Такая технология часто позволяет устранить WAR- и WAW-взаимоза- висимости.

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

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

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

Спекулятивное исполнение

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

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

Программа в такой форме может быть представлена в виде ориентированного графа, как показано на рис. 4.30. Здесь мы вычисляем сумму кубов четных и не­четных целых чисел до какого-либо предела и помещаем результаты в перемен­ные evensum и oddsum соответственно (листинг 4.6). В пределах каждого базового блока технологии, упомянутые в предыдущем подразделе, работают отлично.

Рис. 4.30. Граф базового блока для фрагмента программы, приведенного в листинге 4.6

Листинг 4.6. Фрагмент программы

evesum=0; oddsum=0; i=0;

while (I<1imit) { k=i*i*i;

if(((i/2)*2)—i)

evensum=evensum+k;

else

oddsum=oddsum+k; i=i+l;

}

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

Посмотрите еще раз на рис. 4.30. Представим, что все переменные, кроме evensum и oddsum, были помещены в регистры. Тогда имело бы смысл переместитькоманды LOAD в начало цикла до вычисления переменной к, чтобы выполнение этих команд началось раньше, а полученные результаты были доступны в мо­мент, когда они понадобятся. Естественно, при каждой итерации требуется толь­ко одно значение, поэтому остальные команды LOAD будут отбрасываться, но если кэш-память и основная память конвейеризированы, то подобная процедура име­ет смысл. Выполнение команды раньше того, как станет известно, понадобится эта команда или нет, называется спекулятивным исполнением. Чтобы использо­вать эту технологию, требуется поддержка компилятора, аппаратного обеспече­ния, а также некоторое усовершенствование архитектуры. В большинстве случа­ев переупорядочение команд за пределами одного базового блока находится вне возможностей аппаратного обеспечения, поэтому компилятор должен переме­щать команды явным образом.

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

Однако при наличии спекулятивных команд возникает еще одна проблема, ко­торую нельзя решить путем подмены регистров. Что произойдет, если спекуля­тивная команда вызовет исключение? В качестве примера можно привести коман­ду LOAD, которая вызывает кэш-промах в компьютере со строкой кэша достаточно большого размера (скажем, 256 байт) и памятью, которая работает гораздо мед­леннее, чем центральный процессор и кэш. Если нам требуется команда LOAD и ра­бота машины останавливается на несколько циклов, пока загружается строка кэ­ша, то это не так страшно, поскольку данное слово действительно нужно. Но если машина простаивает, чтобы вызвать слово, которое, как окажется позднее, нам ни к чему, это совершенно нерационально. Если подобных «оптимизаций» слишком много, то центральный процессор будет работать медленнее, чем если бы «опти­мизаций» вообще не было. (Если машина содержит виртуальную память, о кото­рой рассказывается в главе 6, то спекулятивное выполнение команды LOAD может даже вызвать обращение к отсутствующей странице. Подобные ошибки могут зна­чительно повлиять на производительность, поэтому важно их избегать.)

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

Более сложную ситуацию можно проиллюстрировать следующим операто­ром:

if (х > 0) z = у/х;

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

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

Примеры уровня микроархитектуры

В этом разделе в свете материала, изучаемого в этой главе, мы рассмотрим три современных процессора. Наше изложение будет кратким, поскольку компьюте­ры чрезвычайно сложны, содержат миллионы вентилей и у нас нет возможности давать подробное описание. Процессоры, предлагаемые в качестве примеров, те же, что и раньше, — Pentium 4, UltraSPARC III и 8051.

Микроархитектура процессора Pentium 4

На первый взгляд Pentium 4 кажется вполне традиционной CISC-машиной с боль­шим и громоздким набором команд, поддерживающим 8-, 16- и 32-разрядные це­лочисленные операции, а также 32- и 64-разрядные операции с плавающей точ­кой. В нем всего 8 доступных регистров, причем ни один из них не повторяет другие. Допустимая длина команд составляет 1-17 байт. В общем, налицо стан­дартная унаследованная архитектура, которая все делает не так.

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

Микроархитектура Pentium 4, называемая NetBurst, ознаменовала собой реши- тельный отход от принципов микроархитектуры Р6, использовавшейся в про­цессорах Pentium Pro, Pentium II и Pentium III. Она дает определенное представ­ление о том, на какой базе продукция Intel будет разрабатываться в течение нескольких ближайших лет. Примерная схема микроархитектуры Pentium 4 изо­бражена на рис. 4.31. В определенной степени она соответствует рисунку 1.8.

Рис. 4.31. Микроархитектура Pentium 4

Pentium 4 состоит из четырех основных блоков: подсистемы памяти, блока предварительной обработки, блока контроля исполнения с изменением после­довательности и блока исполнения. Рассмотрим эти блоки по порядку, начиная с верхнего левого и продвигаясь против часовой стрелки.

В состав подсистемы памяти входит объединенный кэш второго уровня (L2), а также логика доступа к внешнему ОЗУ по шине памяти. В первом поколении Pentium 4 объем L2 составлял 256 Кбайт; во втором — 512 Кбайт; в третьем — 1 Мбайт. L2 представляет собой 8-входовую ассоциативную кэш-память с 128- байтным строками. Если запрос к кэшу второго уровня не приносит результата, организуются две 64-байтных передачи в основную память, после чего из нее вы­бираются необходимые блоки. Данный кэш L2 относится к категории кэшей с отложенной записью. Иными словами, новые данные в измененной строке за­писываются обратно в память лишь после сброса.

С кэшем тесно связан блок предварительной выборки (он не показан на ри­сунке), который пытается перенести данные из основной памяти в L2 еще до то­го, как эти данные запрошены. Из L2 данные могут на высокой скорости переда­ваться в другие блоки кэш-памяти. За один цикл может быть выполнена одна операция выборки из L2; так, на тактовой частоте 3 ГГц из L2 в другие кэши тео­ретически можно передать до 1,5 млрд 64-байтных блоков в секунду — таким образом, пропускная способность становится равной 96 Гбайт/с.

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