Смекни!
smekni.com

«подклеиванию» (стр. 3 из 7)

Теперь припомним, что уже довольно давно (с микропроцессора Pentium, если быть точнее), с 1993 года, х86-совместимые микропроцессоры стали «суперскалярными». То есть умеют исполнять более одной инструкции за такт. В нашем примере это равносильно существованию двух (трех, и так далее) сборочных конвейеров для автомобилей. Само по себе это усовершенствование подняло производительность микропроцессоров. Но и послужило источником новой проблемы.

Проблема оказалась связана с переменной длиной х86-команд. Это весьма крупное неудобство в плане суперскалярного исполнения. При этом сам перевод х86-инструкций во внутренние команды не вызывает особых трудностей. Неприятность же заключается в необходимости одновременной выборки сразу нескольких команд. Источник неприятности: местоположение второй команды можно определить только после анализа первой (в простейшем случае на это уйдет 1 такт или более, т.о. темп выдачи окажется неприемлемо низким — ~1 команда / такт вместо нескольких). Можно одновременно анализировать сразу байтовый набор в потоке инструкций, но отношение эффективность/затраты оказывается весьма неудовлетворительным. Поэтому используются комбинированные подходы к решению этой проблемы.

Вначале рассмотрим отличия идеологий в Pentium 4 и К7, а затем перейдем к «виновнику торжества» — архитектуре К8. На первом этапе и К7, и Pentium 4 используют довольно простой вариант одновременного анализа команд. А вот позднее пути этих архитектур расходятся.

Pentium 4: базовая концепция состоит в переводе х86-инструкций в более регулярные, «RISC-подобные» микрооперации фиксированной длины.

Выбирая х86-инструкции из куска кода, декодеры переводят х86-инструкции в микрооперации. Чтобы избежать потери времени при повторном просмотре этого же куска, выбирая вторую команду (как мы писали выше, здесь одна из основных трудностей в декодировании), Pentium 4 после перекодирования записывает полученную микрооперацию в Trace cache. Декодеры работают асинхронно (в данном случае имеется ввиду темп, а не частота работы) с исполнительными конвейерами. Все это следствие ключевой концепции микроархитектуры Pentium 4 — достичь как можно большей частоты. Естественно, что специальным образом подготовленные микроинструкции можно исполнять более эффективно и с большим темпом, нежели нерегулярные и весьма разнообразные по форме х86-инструкции. Таким образом, Pentium 4 старается держать как можно больше готовых «переведенных» команд для исполнения в Trace Cache. Да-да, в том самом, который может хранить до 12 000 микроопераций. Ну а поскольку одна х86-команда может превратиться в одну, две, или последовательность микроопераций, теперь бессмысленно оперировать такой привычной нам вещью, как «объем» кэша команд. Ведь в зависимости от того, какие команды нам встретились, количество х86-команд, уместившееся в Trace cache, будет от раза к разу совершенно различным. Таким образом, прямое сравнение «объема» кэша команд между Pentium 4 и его конкурентами становится попросту невозможным.

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

Другими словами, если подытожить, то Pentium 4 заранее превращает х86-инструкции в микрооперации, которые записываются в Trace Cache. Использование Trace Cache является, таким образом, одной из ключевых концепций в Pentium 4.

К7: этот микропроцессор поступает не так. Кэш команд (I-cache) по-прежнему выполняет свое главное назначение — хранит х86-инструкции. В конечном итоге х86-инструкции также превращаются во внутренние команды, но детали этого превращения у К7 отличаются. Выбрав и проанализировав инструкцию, К7(К8) записывает полученную информацию о её границах в специальный битовый массив, Decode Array; инструкция же подвергается дальнейшим преобразованиям во внутренний формат. После этого, при повторном просмотре участка кода, делать самую тяжелую работу по нахождению границ инструкций уже не нужно. Decode Array ассоциирован с I-cache, но физически, на кристалле, располагается отдельно. Каждому байту х86-инструкции (находящейся в I-cache) соответствуют три бита, хранящиеся в Decode Array. Эта запись содержит информацию о том, является ли данный байт первым (последним) байтом инструкции, является ли он префиксом, следует ли направлять инструкцию по особому пути декодирования (подробнее об этом дальше).

Таким образом, мы имеем два совершенно различных подхода: Pentium 4 сохраняет практически результат работы декодера (микрооперацию), в то время как K7/K8 — полезную информацию, существенно облегчающую повторное декодирование. Отсюда автоматически следует, что для того, чтобы система K7/K8 могла использовать все преимущества (и в первую очередь — высокую «емкость» I-кэша), она должна обладать самым совершенным аппаратом, позволяющим проводить повторное декодирование быстро и незаметно. Но мы пока отложим рассмотрение этих методов, так как сейчас нам надо взглянуть на те структурные единицы, которые в конечном итоге будут выданы декодером, и кратко проследить их дальнейшую судьбу.

Первоначальные х86-инструкции на завершающих этапах работы декодера К7/К8 «переводятся» в специальные внутренние команды — макрооперации, или mOP-ы. Большинству х86-инструкций соответствует одна макрооперация, некоторые инструкции преобразуются в два или три mOP-а, а наиболее сложные, например деление или тригонометрические, — в последовательность из нескольких десятков mOP-ов. Макрооперации имеют фиксированную длину и регулярную структуру. В то время как ROP, микрооперация, соответствует одной примитивной команде, посылаемой на исполнение функциональному устройству процессора, mOP соответствует двум командам, которые будут выполнены в двух «спаренных» функциональных устройствах. Очень условно можно считать что в определенный момент mOP может «расщепляться» на два ROP-a, или «выдавать» два ROP-a — по крайней мере, mOP содержит всю необходимую для запуска двух команд информацию, включая служебную. Откуда возникла идея использования более сложной, чем ROP, структурной единицы? Вспомним, что многие х86-инструкции производят сложные действия над значениями в памяти, — действия, включающие не только считывание или запись, но и изменение значения (например, увеличение значения переменной — счетчика, находящейся в памяти). После преобразования такой инструкции во внутренний формат можно либо сразу же дать ROP-ам относительную независимость в плане дальнейшего передвижения по блокам процессора, либо можно объединить всю содержащуюся в них информацию в одной «макрокоманде». В последнем случае мы получим выигрыш не только по количеству «перемещаемых» элементов и, соответственно, логики, отвечающей за такое «перемещение», но и выигрыш за счет того, что сможем существенно сократить число промежуточных операций записи/считывания результата. Наконец, кое-где мы сможем сократить даже число реально выполняемых команд — так, в нашем примере с переменной-счетчиком понадобится всего лишь одно вычисление адреса (вместо двух, как это было бы в случае «разъединенных» ROP-ов). Собственно, и в K7, и в K8 mOP содержит две команды — одну для ALU (или FPU), другую — для AGU (устройства вычисления адреса, Address Generation Unit). Если по каким либо причинам одной команды нет — например, инструкция не обращается к памяти и ей не требуется вычислять адрес — то соответствующие поля mOP-а будут содержать пустышку, NULL-ROP. Обратим внимание, что сошедший с декодера mOP в дальнейшем будет «путешествовать» по конвейеру процессора передвигаясь по своему «каналу» — в конце которого и доберется до пары функциональных устройств (естественно, ALU/FPU и AGU). Точнее, непосредственно перед этим знаменательным событием mOP окажется в очереди (reservation station), где и «выдаст» два нужных ROP-a. ROP-ы будут направляться на исполнение в том порядке, который окажется наиболее удобным, а не в том, какой задает жесткий текст выполняемой программы (конечно же, порядок отправки на исполнение не будет противоречить логике программы, а удобство будет определяться готовностью операндов и занятостью функциональных устройств, ФУ). Поэтому вполне допустимы ситуации, когда между выполнением ROP-ов, относящихся к одному mOP-у, будут выполняться и другие ROP-ы, порожденные совсем другими инструкциями — предшествующими нашей инструкции, или следующими за ней. И, наконец, заметим, что соотнесение 1mOP = ROP для ALU(FPU) + ROP для AGU — лишь частный случай более общей концепции: в последующих поколениях процессоров может быть добавлено, например, третье устройство на «канал» — и mOP уже будет иметь более сложную структуру.

Хорошо. Запуская mOP по «каналу» мы сможем в конечном итоге добиться одновременного запуска двух команд. Как достичь большего? Правильно — добавим параллельные каналы, и одновременно на каждый из них выдадим по макрооперации. Так поступила AMD в процессорах К7/К8. Каналов — три, они симметричны, каждый содержит свою очередь и свою пару функциональных устройств. В результате можно одновременно запускать команды сразу на шесть функциональных устройств (ФУ).

Мы говорим именно про отправку команд на исполнение, так как за счет конвейеризации возможны ситуации, когда одновременно в разных блоках процессора будут выполняться и два десятка команд. Также для полноты картины заметим, что и в К7, и в K8 имеется десять функциональных устройств — три ALU, три FPU, три AGU и отдельный блок умножения. Каждый канал разветвляется: в зависимости от того, какие значения обрабатывает инструкция, mOP пойдет либо по направлению к целочисленным блокам, либо — к блокам плавающей точки; блок же AGU останется общим.