В самом деле, если пример, приведенный выше, переписать как
MOV AX,1234h
ADD DX,1234h
мы получим идентичный результат, но обе команды могут быть исполнены параллельно всего за один такт процессора. Можно пойти дальше и задержать выполнение первой инструкции до той поры, пока значение регистра AX не потребуется в явном виде. Если оно и вовсе никогда не потребуется - мы сэкономим целый такт!
Идеи о подобной, на лету, оптимизации кода породили суперскалярные микропроцессоры, то есть такие, где параллелизм команд явно не указан и отслеживается процессором самостоятельно.
Однако Intel нашла подобные приемы оптимизации слишком трудными для реализации и сделала упор на изменение порядка выполнения команд. Так, последовательность
MOV AX,1234h ; Записать в регистр AX число 1234h
ADD DX,AX ; Сложить содержимое регистра DX с регистром AX
MOV CX,666h ; Записать в регистр CX число 666h
ADD BX.CX ; Сложить содержимое регистра BX с регистром CX
Можно исполнить в другом порядке:
MOV AX,1234h ; Записать в регистр AX число 1234h
MOV CX,666h ; Записать в регистр CX число 666h
ADD DX,AX ; Сложить содержимое регистра DX с регистром AX
ADD BX.CX ; Сложить содержимое регистра BX с регистром CX
Теперь соседние инструкции независимы и могут быть исполнены параллельно. Следовательно, приведенный выше пример может быть исполнен за два такта вместо четырех. Очень неплохой путь повышения производительности, но, к сожалению, очевидно тупиковый: усовершенствовав интеллектуальный "движок", можно найти способ параллельного исполнения четырех команд, но сомнительно, чтобы существовал волшебный способ устранения зависимости между восемью и более командами.
RISC в этой ситуации оказались в более выигрышном положении. Ограниченный набор регистров CISC порождал проблемы аналогично следующей:
MOV AX,1234h ; Записать в регистр AX число 1234h
ADD DX,AX ; Сложить содержимое регистра DX с регистром AX
MOV AX,666h ; Записать в регистр CX число 666h
ADD BX.AX ; Сложить содержимое регистра BX с регистром AX
Теперь уже невозможно одновременно выполнить первую и третью строки, однако этой, казалось бы, на первый взгляд, неразрешимой проблеме быстро было найдено красивое решение. В действительности зависимость между двумя командами ложная. Очевидно (даже машине), что должны использоваться дополнительные регистры. Но что нас ограничивает? Давайте переименуем регистры в AX~1 и AX~2 соответственно. Тогда получим следующий код:
MOV AX~1,1234h ; Записать в регистр AX число 1234h
ADD DX, AX~1 ; Сложить содержимое регистра DX с регистром AX
MOV AX~2,666h ; Записать в регистр CX число 666h
ADD BX, AX~2 ; Сложить содержимое регистра BX с регистром AX
Разумеется, теперь никаких проблем с параллельным исполнением уже не возникнет. Конечно же, потребуется больше регистров! Но регистры дешевы (всего лишь набор триггеров из 4-6 транзисторов), а за быстродействие потребитель деньги охотно заплатит.
Проблема в том, что добавление новых регистров потребует перекомпиляции всего существующего программного обеспечения и изменения адресации. Одним словом, приведет к несовместимости с предыдущими моделями. Для кого-то это, может, было и не критично, но только не для Intel, которой обратная совместимость нужна обязательно.
И Intel находит блестящее решение. Существующие регистры при декодировании команды проецируются на гораздо больший набор внутренних. И как только обнаруживается ложная зависимость, очередной регистр переименовывается. Разрыв в производительности между CISC и RISC снова утерян.
ПАРАЛЛЕЛИЗМ В МИКРОПРОЦЕССОРАХ RISC и CISC
Микропроцессоры PowerPC (RISC)
Процессор PowerPC был разработан в результате тесного сотрудничества ведущих производителей индустрии - IBM, Apple и Motorola. Он воплотил в себе гений тысяч инженеров, долгое время был лидером среди собратьев в отношении цена/производительность.
Суперскалярное RISC-ядро позволяло за один такт выполнять до четырех команд благодаря четырем конвейерам выборки и шести независимым функциональным устройствам: трем целочисленным АЛУ, блоку вещественной арифметики (обрабатывающему числа с плавающей точкой), модулю чтения\записи результатов и блоку переходов. Для параллельного исполнения выборка инструкций должна соответствовать набору имеющихся устройств. Четыре целочисленные операции за один такт не выполнятся, а вот три целочисленные и одна вещественная могут.
Для повышения производительности и поддержки эффективной загрузки функциональных блоков в PowerPC используется динамическое предсказание условных переходов вкупе с упреждающим выполнением кода на глубину до четырех предсказанных переходов.
При этом результаты работы команд записываются в промежуточный кэш-буфер, который обнуляется в случае ошибочного предсказания. Что представляет собой динамическое предсказание ветвлений? В микропроцессорах PowerPC использован простой и надежный механизм таблицы предыстории переходов (или, говоря красивым техническим языком, BTH - Branch History Table).
В кэш этой таблицы заносятся адреса и результаты ветвления. Если попадается условный переход, который уже был занесен в таблицу, процессор полагает, что результат ветвления окажется тот же самый, и начинает упреждающее выполнение соответствующей ветви.
Этот механизм позволяет угадывать направление перехода в среднем в 95 случаях из 100, что значительно выше, чем у CISC-микропроцессоров. Впрочем, и самих условных переходов в RISC'ах значительно больше, да и находятся они большей частью в циклах, тогда как в CISC они иррегулярно разбросаны по всему коду и имеют тесную связь между обрабатываемыми в настоящий момент данными и направлением ветвления. Попросту говоря, они менее периодичны и предсказуемы. Но ввиду того, что самих условных переходов значительно меньше, удачность или неудачность предсказаний незначительно отражается на общей производительности.
PowerPC активно использует технологию переупорядочения последовательности выполнения команд. В любой момент могут быть выполнены четыре произвольные инструкции из шестнадцатиэлементного буфера команд.
Микропроцессор Pentium Pro
Высокая производительность была достигнута в большой степени благодаря улучшению внутреннего параллелизма архитектуры процессора. Прежде всего, это разнесенная архитектура и динамическое исполнение команд.
Последнее представляет собой опережающее исполнение команд, предсказание переходов и переупорядочение очереди выполнения инструкций. Кроме того, в Pentium широко используется нехарактерная для RISC технология переименования регистров для устранения ложной зависимости (в RISC просто слишком много регистров, чтобы в этом возникла существенная необходимость или оказалось недостаточно переупорядочения команд).
Конвейеров у Pentium Pro два. Однако это еще не означает, что Pentium способен выполнить две любые инструкции параллельно. Исходя из сложности команд CISC и того факта, что каждая инструкция может задействовать произвольное (читай: временами очень большое) число функциональных устройств микропроцессора, их все пришлось бы дублировать. В результате далеко не всякие инструкции выполняются параллельно.
Для борьбы с иррегулярными переходами в Pentium был использован значительно более сложный по сравнению с RISC-системами статически-динамический анализатор условия ветвления вместе с детектором вложенных конструкций (например, вложенных циклов). Но даже этот механизм не позволил достичь свыше 80 процентов угадываний.
Видно, что у RISC-микропроцессоров параллелизм выражен в большей степени, чем у CISC. Фиксированная (или близкая к ней) длина инструкций облегчила разработку многоконвейерных реализаций. Простые инструкции по отдельности задействовали лишь одно функциональное устройство. Например, типичная CISC-операция:
ADD AX, [SI+BX+66h]
Необходимо разобрать сложное поле адресации, чтобы понять, что это именно "SI+BX+66h", а не что-то другое; сложить (то есть вычислить этот результат), затем загрузить содержимое требуемой ячейки и, наконец, опять сложить его со значением регистра AX.
Следовательно, для выполнения инструкции за один такт требуется два арифметических устройства и один загрузчик. Но и в этом случае все действия должны выполняться последовательно. То есть ВЫЧИСЛЕНИЕ АДРЕСА, ЗАГРУЗКА, ВЫЧИСЛЕНИЕ РЕЗУЛЬТАТА. И никакие действия не могут быть спарены. Хоть умри, но потребуется три такта на выполнение, или хотя бы одно из устройств должно функционировать на удвоенной тактовой частоте.
RISC казались более приятным полигоном для проникновения параллельных вычислений в бытовые компьютеры. А в том, что будущее принадлежит параллельным архитектурам, уже никто не сомневался: линейные (последовательные) вычисления наткнулись на непреодолимые на сегодняшнем этапе физические и технические ограничения и к середине девяностых сохранились только там, где производительность была некритична (скажем, микрокалькуляторы, наручные часы, аппаратура определения номера и др.).
ВЫБОР ДРУГОГО ПУТИ
Производители CISC-архитектур недолго оставались в тупике. Они решили усилить параллелизм не в исполнении команд, а в обработке данных, введя поддержку векторных операций. Именно в этом и заключается широко известная MMX-технология.
Однако новая технология не давала никакого выигрыша на старых приложениях - не спасала даже перекомпиляция. Существенно повлиять на быстродействие могло только принципиальное изменение идеологии программирования: переход на векторные операции.
Несомненный плюс - явный параллелизм всего в одной инструкции там, где раньше их требовалась сотня. Использование MMX дает до 60 процентов прироста производительности приложений, манипулирующих графикой, звуком или аналогичными вычислениями и обработкой больших массивов чисел.