Журнал Компьютерра - Журнал «Компьютерра» №37
Сгенерированные линии от VectorPath- и DirectPath-декодеров по одной за такт поступают в специальное устройство - Instructions Control Unit (ICU), где подготовленные к исполнению линии накапливаются в специальной очереди (24 линии). О том, что происходит дальше, поясним с помощью аналогии.
Предположим, что наша программа - это книжка, в которой записано, как процессору нужно обрабатывать данные. Что делает процессор? Упоминавшийся блок выборки вырывает из книжки страничку с текстом (будем считать, что странички достаточно маленькие) и выбирает из нее от одной до трех содержательных частей, которые передает декодеру. Декодер читает выделенные фрагменты текста и конвертирует их в четкие инструкции, указывающие, что и в какой последовательности нужно сделать. Инструкции (по одной) он записывает на бумажках (mOP’ах) и упаковывает в конверты - до трех бумажек в один конверт (линию). Конверты поступают в специальную картотеку - ICU, где их вскрывает и прочитывает специальный человек.
Что дальше? Претендентов на декодированные инструкции два - блок целочисленных вычислений (ALU) и блок вычислений с плавающей точкой (FPU). Когда блоки готовы принять очередную инструкцию, они сообщают об этом человеку в картотеке; человек копается в своих конвертах и выбирает из них в произвольном порядке, как ему удобнее, до трех бумажек-инструкций, которые и раздает ALU и FPU. Единственное ограничение, которое при этом накладывается, - человек никогда не передает ALU и FPU те инструкции, выполнение которых зависит от еще не переданных. Блоки ALU/FPU каким-то хитрым образом выполняют полученные инструкции, но результаты отсылают не во «внешний мир», а в нашу картотеку-ICU, где их кладут в тот же самый конверт, в котором лежали инструкции. Даже если происходит ошибка выполнения, процессор не сообщает о ней сразу, а сперва записывает информацию об ошибке на конверте; когда настанет пора вскрыть конверт - вот тогда он про нее и сообщит. Чтобы потом эти данные использовать - применяется довольно хитрая техника (та самая, из сноски 4), позволяющая вновь выполняемым инструкциям обращаться к еще «официально несуществующим» данным. Когда для конверта все инструкции оказываются выполненными, а конверт стоит первым в очереди и больше не содержит инструкций, но лишь результаты их исполнения - то полученные результаты «объявляются официальными», а конверт выбрасывается (отставка линий). Иногда, если при вскрытии очередного конверта выясняется, что ранее была допущена ошибка при предсказании условного перехода или при выполнении содержащейся в конверте инструкции, дело до этого и не доходит - конвейер приходится «сбрасывать», то есть смотреть на последнем конверте адрес того самого неудачного перехода, выкидывать всю накопленную к текущему моменту картотеку со всеми ее результатами и начинать выполнение с того самого места, где произошло неверное предсказание перехода. Благодаря тому, что результаты выброшенных конвертов еще не были «объявлены официальными», а «рвем» мы конверты строго в той же очередности, в которой они к нам в очередь поступали - допущенная ошибка «никому не станет известна» - результаты выполнившихся «вперед батьки» инструкций автоматически будут аннулированы.
Если теперь вернуться к технологическому описанию конвейера, то изложенный выше процесс с конвертами происходит следующим образом. Из очереди в 24 линии по три mOP’а в каждой ICU выбирает в наиболее удобной для исполнения последовательности один-три mOP’а и пересылает их либо на ALU, либо на FPU - в зависимости от типа микрооперации. В случае ALU микрооперации сразу же попадают в очередь планировщика (шесть элементов по три mOP’а), который подготавливает необходимые для исполнения микрооперации ресурсы, дожидается их готовности и только потом отправляет mOP вместе со всеми необходимыми данными на исполнение. Причем при исполнении одного mOP’а на самом деле может происходить исполнение сразу двух действий - несложных арифметических вычислений, которые часто возникают при обращении к оперативной памяти (ими занимается блок Address Generation Unit, AGU), и «сложных», требующих вмешательства «полновесного» ALU, - соответствующая «двойка» микроинструкций (ROP) закладывается в mOP еще на стадии декодирования. Подготовка данных в планировщике занимает (в идеальном случае) один такт, исполнение - от одного (подавляющее большинство инструкций) до трех (при обращении к оперативной памяти) и даже пяти (64-битное умножение) тактов.
С блоком FPU все чуточку сложнее. Для начала вышедшие из ICU mOP’ы проходят две стадии по подготовке их операндов. Затем - накапливаются в планировщике FPU (двенадцать элементов по три mOP’а), который, по аналогии со своим целочисленным собратом, дожидается, пока данные для этих mOP’ов будут готовы, а исполнительные устройства освободятся, и разбрасывает накопленные mOP’ы по трем исполнительным устройствам. Но в отличие от целочисленной части конвейера (где содержатся по три одинаковых блока ALU и AGU), исполнительные устройства FPU «специализированы» - каждое производит только свой специфический набор действий над числами с плавающей запятой. Время выполнения: два такта на переименование и отображение регистров, один такт (в идеале) на планирование и ожидание операндов, четыре такта на собственно исполнение.
Финал же у всех закончившихся микроопераций один - они «возвращаются» в ICU с полученными результатами, и ICU, по мере готовности линий, потихоньку производит их отставку. На все про все в идеальных условиях у нас ушло 10-17 тактов, причем за каждый такт мы исполняли по три mOP’а (это обычно 1,5-3 инструкции x86).
Устройство процессоров Intel архитектуры NetBurst
Архитектура NetBurst сегодня лежит в основе всех процессоров Pentium 4, Xeon и Celeron. Эффективная длина конвейера в зависимости от варианта составляет 20 или 31 стадию. Количество одновременно исполняемых инструкций за такт в устоявшемся режиме - до четырех; тактовые частоты серийно выпускаемых процессоров - от 2,53 до 3,8 ГГц - это по всем показателям лучше данных по K8. Лучше, но, к сожалению, только сугубо теоретически и на специально подготовленном коде.
***
NetBurst тщательно оптимизировалась для работы на высоких частотах, и назвать эту архитектуру классической можно только с большой натяжкой. Для начала упомянем хотя бы тот же Trace Cache (TC), заменяющий в NetBurst классический Гарвардский I-cache (L1 code). Идея состоит в том, что в NetBurst декодер вынесен за пределы собственно конвейера - процессор конвертирует x86-инструкции в свое внутреннее представление не на лету, как AMD K8, а заблаговременно, еще на стадии копирования кода в кэш-память первого уровня. Устроено это все так своеобразно (например, в процессе декодирования декодер убирает безусловные переходы, занимается предсказанием условных переходов и может едва ли не «разворачивать» циклы!), что внутреннему устройству Trace Cache и декодеру инструкций для него вообще можно посвятить отдельную статью (чего мы делать сейчас не будем; скажем только, что декодер для TC работает очень медленно). Точная длина соответствующего участка конвейера неизвестна, но составляет, по разным оценкам, от 10-15 до 30 тактов - то есть этот «скрытый» участок конвейера имеет длину едва ли не большую, чем «видимый». Таким образом, введение TC позволяет практически вдвое уменьшить эффективную длину конвейера (страшно даже представить NetBurst без Trace Cache)[С K8, кстати, та же самая история - декодированием и подготовкой инструкций занята примерно половина конвейера. Есть предположения, что в следующем поколении процессоров AMD - архитектуре K9 - появится и Trace Cache. Скажем, в K8 подобное нововведение уменьшило бы видимую длину конвейера до 6-7 стадий в целочисленных вычислениях и до 12 стадий в вычислениях с плавающей точкой!]! Емкость TC для всех NetBurst составляет 12 тысяч микроопераций; в терминах классического x86 это соответствует примерно 8-16 Кбайт кэша L1-data; причем работает TC и обслуживающая его логика на половинной частоте ядра и наполняется декодером с темпом не более одной новой инструкции за такт. Поэтому если процессор некстати вылетит на незакэшированный участок кода (а кэш маленький, и подобная ситуация вполне возможна), то от теоретически возможных четырех инструкций за такт в лучшем случае останется лишь одна. Подобные «резкие потери темпа» вообще свойственны архитектуре NetBurst; к счастью, такие ситуации возникают редко.
Дальнейшее повествование я буду вести, указывая время исполнения инструкции для ядра Northwood (20-стадийный конвейер). Для более нового Prescott в целом справедливо все то же самое, просто время исполнения отдельных стадий слегка возросло.
Первые четыре такта работы конвейера - извлечение специальным блоком выборки инструкций из TC и второй этап предсказания условных переходов. В первый раз декодер TC уже пытался предсказать переход, так что второй этап предсказания фактически сводится к «угадыванию» того, правильно ли декодер угадал переход еще «в тот раз» или нет. Заодно для некоторых записей TC («закладок»["Закладки" позволяют увеличить эффективный объем Trace Cache, поскольку вместо нескольких mОР’ов мы храним в нем одну «закладку»]) происходит их «развертывание» в несколько микроопераций. В силу того что TC работает на половинной частоте ядра, происходит выборка довольно медленно и каждый ее этап занимает по два такта конвейера. Затем полученные микрооперации (до шести штук за такт) складываются в традиционную очередь выборки (Fetch Queue), где буферизуются, сглаживая неравномерность декодирования и обеспечивая «на выходе» устоявшийся темп декодирования в три микроинструкции за такт. Задержка, вносимая буферизацией, - 1 такт; еще 1 такт расходуется на то, чтобы подготовить внутренние ресурсы процессора для выбранной из Fetch Queue тройки mOP’ов. Затем еще два такта уходит на то, чтобы подготовить для каждого mOP’а персональные физические регистры для вычислений (в рамках техники переименования регистров). И, наконец, на последнем, девятом по счету такте полностью готовые к исполнению mOP’ы начинают «распихиваться» по очередям инструкций, стоящих на выполнение.