Здравствуйте! Когда появится возможность сдать экзамен на сертификат? |
Конвейеризация
Определение конвейера; конфликты при конвейерном исполнении машинного кода; особенности обработки конфликтов в CICS- и RISC-архитектурах; пример генерации машинного кода для CISC-процессора ARM и RISC-процессора MIPS для демонстрации приёмов обработки потенциальных конфликтов конвейеризации на уровне компиляции; параллельное исполнение команд на примере процессоров Intel x86
ОПРЕДЕЛЕНИЕ КОНВЕЙЕРА
Команды процессора часто задействуют значительное количество блоков процессора и исполняются за несколько тактов. Например, рассмотрим команду сложения числа из регистра с числом из памяти dd eax, DWORD PTR [ebp-0x8] в процессоре Intel 80386. Эта команда выполняется следующим образом:
- в начале выполняется чтение числа из регистра eax в арифметико-логическое устройство (АЛУ);
- значение адресного регистра ebp и константа -0x8 подаются на вход адресному сумматору (заметим, что здесь речь не о сумматоре, входящем в состав АЛУ, а об отдельном адресном сумматоре, предназначенном для арифметических действий над адресами; различных специализированных сумматоров в процессоре может быть достаточно много);
- вычисляется адрес в памяти для второго операнда;
- выполняется чтение по этому адресу числа из памяти во временный регистр, соединённый с АЛУ;
- выполняется сложение двух чисел;
- результат записывается в регистр eax.
Таким образом эта команда выполняется за 6 тактов (следует отметить, что этот пример существенно упрощён для наглядности). При этом, если не использовать дополнительных оптимизаций, то на каждом такте задействуется лишь часть блоков процессора, а остальные простаивают. Например, при сложении двух значений, которые находятся в регистрах процессора, блок работы с памятью бездействует.
Конвейер - это механизм, предназначенный для распараллеливания выполнения команд программы между блоками процессора. Он позволяет загрузить блоки процессора при выполнении команд оптимально, без простоев.
Приведём пример конвейера. Рассмотрим упрощённый процессор, у которого каждая команда исполняется в три этапа, точнее, за три такта:
- чтение аргументов команды из памяти или регистра (ЧТН),
- исполнение команды (ИСП),
- запись результата в память или регистр (ЗАП).
На рис.10.1 изображен процесс выполнения этим процессором последовательности команд Команда 1, Команда 2, Команда 3, Команда 4, Команда 5 с использованием конвейера. Рассмотрим четвёртый такт конвейера, который выделен на рисунке. Команда 1 уже полностью выполнена, Команда 2 находится на этапе записи данных (ЗАП), Команда 3 - на этапе исполнения (ИСП), Команда 4 - на этапе чтения данных (ЧТН), а к выполнению Команды 5 процессор ещё не приступал. Из-за того, что на каждом такте процессор выполняет с помощью конвейера сразу несколько частей (этапов) команд, все пять команд выполняются за 7 тактов, а не за 15, как было бы при последовательном исполнении этой же цепочки команд. Это оказывается возможным в виду того, что процессор параллельно выполняет различные этапы команд.
КОНФЛИКТЫ ПРИ КОНВЕЙЕРНОМ ИСПОЛНЕНИИ МАШИННЫХ КОМАНД
Результаты выполнения команд используются следующими командами программы. Посмотрим, как это простое соображение согласуется с конвейером.
Приведённый выше пример показывает, что Команда 3 не сможет использовать результаты Команды 2, поскольку свои входные данные она читает раньше, чем Команда 2 записывает свои результаты. В том случае, когда Команде 3 действительно нужны результаты Команды 2, Команда 2 и Команда 3 называются зависимыми.
Ситуации, возникающие при конвейеризованном исполнении команд, которые препятствуют корректному выполнению очередной команды, называются конфликтами.
Конфликты бывают следующих видов.
- Конфликт по данным между зависимыми машинными командами заключается в том, что на конвейере одновременно находятся на разных стадиях выполнения команды, которые могут быть корректно исполнены лишь строго последовательно.
- Конфликт по ресурсам возникает в ситуации, когда двум командам на конвейере одновременно нужен доступ к какому-либо блоку процессора, с которым в один момент времени может работать только одна команда.
- Конфликт по управлению заключается в том, что следующая команда на конвейере является условным переходом, но условие для него ещё не вычислено предыдущей командой и не понятно, какую ветку условного оператора следует загружать на конвейер.
Поскольку конфликты по данным являются распространённой на практике проблемой, приведём ряд примеров таких конфликтов:
- следующая команда на конвейере должна читать данные на выходе предыдущей, но предыдущая ещё не закончила их обработку: при этом нарушается зависимость "чтение после записи";
- следующая команда записывает данные, но они используются (читаются) предыдущей командой, то есть при этом нарушается зависимость "запись после чтения";
- следующая команда записывает (в регистры процессора или в оперативную память) свои результаты раньше предыдущей, из-за чего предыдущая потом может перезаписать эти результаты: при этом нарушается зависимость "запись после записи".
Сделаем замечание относительно третьего вида конфликтов (по управлению) отметим. Такие конфликты дополнительно осложняются тем обстоятельством, что заранее не ясно, в какое место программы произойдёт условный переход, и поэтому не понятно, из какой его ветки загружать на конвейер следующие команды. В связи с этим во многих процессорах есть система предсказания условных переходов, которая собирает статистику по переходам и выбирает наиболее вероятный вариант. В случае ошибки предсказания конвейер очищается от частично обработанных команд, что вызывает задержки.
Для того чтобы программа могла работать корректно с применением конвейера, конфликтные ситуации требуется обрабатывать, то есть обнаруживать и разрешать. Обнаружение конфликтов оказывается непростой задачей, которую мы не будем здесь рассматривать. Разрешение конфликтов может выполняться различными способами. Самым простым способом является "торможение" конвейера (pipeline stall) с целью задержки выполнения следующей команды до момента исчерпания конфликта. Но, во-первых, конфликтную ситуацию требуется обнаружить, что не просто, во-вторых, задержки уменьшают преимущества конвейера.
Выделим следующие подходы к обработке конфликтов.
- Статическое переупорядочивание машинных команд при компиляции программ с языков высокого уровня в машинный код.
- "Разнесение" конфликтующих команд при компиляции на безопасное расстояние друг от друга с помощью вставки необходимого количества специальной команды NOP (No Operation). Команда NOP ничего не делает, но замедляет работу программы на один такт, в некоторых процессорах - и на большее число тактов.
- Динамическая обработка конфликтов во время исполнения программы - идентификация и разрешение конфликтов выполняется в момент выполнения программы. При этом процессор задерживает выполнение зависимых команд (как, например, Intel 80486), а также самостоятельно переупорядочивает команды, чтобы исключить конфликты с минимизацией потери времени (так действуют процессор Intel Pentium и последующие процессоры семейств Intel x86).
ОСОБЕННОСТИ ОБРАБОТКИ КОНФЛИКТОВ В CISC- И RISC-АРХИТЕКТУРАХ
Рассмотрев общие подходы к обработке конфликтов на конвейерах, остановимся теперь на особенностях этой процедуры в RISC- и CISC-процессорах. Свойства машинного языка оказывают определяющее влияние на то, каким образом можно организовать конвейерное исполнение машинного кода. Как следствие, различия между CISC- и RISC-процессорами приводят к использованию различных подходов к обработке конфликтов.
CISC-процессоры делают акцент на динамической обработке конфликтов, поскольку в CISC-процессорах команды имеют различную сложность и время работы, задействуют разные блоки процессора и обращаются к операндам различного вида. Кроме того, в некоторых семействах динамическая обработка конфликтов является единственным возможным решением, поскольку механизм конвейеризации в них появился не сразу, и требовалась совместимость с предыдущими версиями процессоров, то есть механизм конвейеризации не является глубоко интегрированным в процессор.
Рассмотрим пример. Первым конвейеризованным процессором семейства Intel x86 был процессор Intel 80486. Для того, чтобы у него была возможность корректно и по возможности быстро исполнять машинный код, предназначенный для предыдущих версий не конвейеризованных процессоров, ничего не оставалось, кроме как "научиться" разрешать конфликты динамически. Ни на какие "подсказки" компилятора он при этом рассчитывать не мог, ведь существующий машинный код для предыдущих процессоров семейства Intel x86 про конвейеризацию ничего не знал.
Реализация динамической обработки конфликтов в CISC-процессорах требует значительного усложнения конвейера. Для сравнения можно представить себе, как усложнился бы конвейер на машиностроительном заводе, если бы на нём приходилось одновременно собирать бульдозеры, комбайны и танки, вдобавок разных моделей. Отметим, что иногда компиляторы и низкоуровневые программисты всё же помогают CISC-процессорам, создавая максимально "безконфликтный" машинный код. Это положительно сказывается на скорости работы программ на старых процессорах, которые, как, например, Intel 80486, умеют при обнаружении конфликтов лишь задерживать выполнение команд, но не переупорядочивать их.