Оптимизация для процессоров семейства Pentium: 16. Переименование регистров (PPro, PII и PIII) — Архив WASM.RU
16.1 Уничтожение зависимостей
Переименование регистров - это продвинутая техника, используемая этими микропроцессорами, чтобы убрать зависимости между различными частями кода. Пример:
Код (Text):
MOV EAX, [MEM1] IMUL EAX, 6 MOV [MEM2], EAX MOV EAX, [MEM3] INC EAX MOV [MEM4], EAXЗдесь последние три инструкции независимы от трех первых в том смысле, что им не требуется результат, полученный после их выполнения. Чтобы оптимизировать этот код на ранних процессорах вы были должны использовать другой регистр, отличный от EAX, в последних трех инструкциях и перегруппировать инструкции так, чтобы последние три выполнялись параллельно с первыми тремя. PPro, PII и PIII делают это автоматически. Они назначают новый временный регистр для EAX каждый раз, когда вы пишете в него. Таким образом инструкции 'MOV EAX, [MEM3]' становятся независимыми от предшествующих инструкций. С помощью выполнения не по порядку 'MOV [MEM4], EAX' может выполниться до того окончания обработки медленной инструкции IMUL.
Переименование регистров происходит полностью автоматически. Новый временный регистр назначается как псевдоним постоянному регистру каждый раз, когда инструкция пишет в этот регистр. Например инструкция 'INC EAX' использует один временный регистр для ввода и другой временный регистр для вывода. Это, разумеется, не убирает зависимости, но имеет некоторое значение для последующих чтений из регистра, о чем я расскажу позже.
Все регистры общего назначения, указатель на стек, флаги, регистры плавающей запятой, регистры MMX, регистры XMM и сегментные регистры могут быть переименованы. Контрольные слова и слово статуса плавающей запятой не могут быть переименованы, поэтому навряд ли у вас кончаться временные регистры.
Общей практикой установки значения регистра в ноль является 'XOR EAX,EAX' или 'SUB EAX, EAX'. Эти инструкции не распознаются как независимые от предыдущего значения регистра. Если вы хотите убрать зависимость от медленных предшествующих инструкций, используйте 'MOV EAX, 0'.
Переименование регистров контролируется таблицей псевдонимов регистров (RAT) и буфером перегруппировки (ROB). Мопы из декодеров поступают в RAT через очередь, затем в ROB, а после чего в резервационную станцию (reservation station). RAT может обрабатывать только 3 мопа за такт. Это означает, что суммарная производительность процессора не может превышать 3 мопа за такт.
Практически нет никаких ограничений на количество переименований. RAT может переименовывать три регистра за такт, и он может даже переименовать один и тот же регистр три раза за один такт.
16.2 Задержки чтения регистров.
Но существует другое ограничение, которое может быть весьма серьзеным, и это то, что за один такт вы можете читать только из двух постоянных регистров. Это ограничение относится ко всем регистрам, используемых инструкциям, не считая тех регистров, в которые инструкции только пишут. Пример:
Код (Text):
MOV [EDI + ESI], EAX MOV EBX, [ESP + EBP]Первая инструкция генерирует два мопа: один считывает EAX, а другой - EDI и ESI. Вторая инструкция генерирует один моп, который читает ESP и EBP. EBX не учитывается, поскольку инструкция только пишет в него. Давайте предположим, что эти три мопа идут вместе через RAT. Я буду использовать слово триплет для группы из трех последовательных мопов, которые идут вместе через RAT. Так как ROB может обрабатывать только два чтения из постоянных регистров за такт, а нам нужно пять чтений, то наш триплет будет задержен на два дополнительных такта, прежде чем он попадет в резервационную станцию. При трех или четырех чтений из постоянных регистров он был бы задержен на один такт.
Из одного регистра в одном триплете можно читать больше одного раза без ущерба для качества. Если вышеприведенные инструкции поменять на:
Код (Text):
MOV [EDI + ESI], EDI MOV EBX, [EDI + EDI]тогда произойдет только два чтения из регистров (EDI и ESI) и триплет не будет задержен.
Регистр, в который будет произведена запись текущим мопом, сохраняется в ROB, поэтому из него можно свободно читать, пока он не будет выгружен оттуда, что занимает по меньшей мере три такта, а обычно еще больше. Выгрузка из ROB является финальной стадией выполнения, когда значение становится доступным. Другими словами, вы можете читать любое количество раз из регистра в RAT без задержек, если их значение еще не стало доступным у модулей выполнения, поэтому вы можете быть уверены, что из регистра, в который была произведена в одном триплете, можно свободно читать как минимум в трех последующих. Если выгрузка (writeback) была задержена перегруппировкой, медленными инструкциями, цепочками зависимости, задержкой кэша или по какой-то другой причине, тогда из регистра можно свободно читать еще некоторое время.
Пример:
Код (Text):
MOV EAX, EBX SUB ECX, EAX INC EBX MOV EDX, [EAX] ADD ESI, EBX ADD EDI, ECXЭти шесть инструкций генерирую 1 моп каждая. Давайте предположим, что первые три мопа идут через RAT вместе. Эти три мопа читают регистры EBX, ECX и EAX. Но так как мы пишем в EAX до того, как начали из него читать, чтение прозводится "бесплатно" и у нас нет никаких задержек. Следующие три мопа читают EAX, ESI, EBX, EDI и ECX. Так как оба EAX, EBX и ECX были изменены предыдущим триплетом и не были еще выгружены, из них можно свободно читать, поэтому учитываются только ESI и EDI, и у нас нет задержек и во втором триплете. Если инструкцию 'SUB ECX, EAX' в первом триплете заменить на 'CMP ECX, EAX', тогда в ECX не производится запись, и у нас происходит задержка во втором триплете при чтении ESI, EDI и ECX. Подобным же образом, если инструкция 'INC EBX' в первом триплете поменять на NOP или что-нибудь вроде этого, тогда у нас будет задержка во втором триплете при чтении ESI, EBX и EDI.
Ни один моп не может читать больше, чем из двух регистров. Поэтому все инструкции, читающие больше, чем из двух регистров, разбиваются на два или больше мопа.
Чтобы подсчитать количество прочитываемых регистров, вам нужно включить все регистры, которые считываются инструкцией. В это число входят все целочисленные регистры, флаговые регистры, указатель на стек, регистры плавающей запятой и регистры MMX. Регистр XMM идет за два, кроме тех случаев, когда используется только его чать, например в ADDSS и MOVHLPS. Сегментные регистр и указатель на инструкцию не учитываются. Например в 'SETZ AL' вы считаете флаговый регистр, но не AL. В 'ADD EBX, ECX' считаются и EBX и ECX, но не регистр флагов, потому что в него только производится запись. 'PUSH EAX' читает EAX и указатель на стек, а потом пишет в последний.
Инструкция FXCH является особым случаем. Она работает с помощью переименования, но не считывает никаких значений, поэтому она не попадает под действие какого-либо правила о задержке чтения из регстра. Инструкция FXCH ведет себя как один моп, который ни читает, ни пишет в регистр, когда дело касается правил задержек чтения из регистра.
Не путайте триплеты мопов с раскодировываемыми группами. Последняя может генерировать от одного до шести мопа, и даеж если раскодировываемая группа имеет три инструкции и генерирует три мопа, нет никакой гарантии, что эти три мопа попадут в RAT вместе.
Очередь между декодерами и RAT так которотка (10 мопов), что вы не можете предположить, что задержки чтения регистров не оказывают влияния на декодеры, или какие-то изменения в выводе декодеров не задерживают RAT.
Очень трудно предсказать, какие мопы пойдут через RAT вместе, если очередь не пуста, и для оптимизированного кода очередь должна быть пуста только после неправильно предсказанного прехода. Несколько мопов, генерируемых одной инструкцией не обязательно пойдут через RAT вместе; мопы просто берутся последовательно из очереди по три за раз. Последовательность не нарушается предсказанным переходом: мопы до и после перехода могут пойти через RAT вместе. Только неправильно предсказанный переход сбросит очередь и начнет все сначала, поэтому три следующих мопа точно попадут в RAT вместе.
Если три последовательных мопа читают больше, чем из двух регистров, то вы бы, конечно, предпочли, чтобы они не шли вместе друг с другом в RAT. Вероятность того, что они пойдут вместе - одна третья. Задержка чтения трех или четырех выгруженных регистра в одной тройке мопов - один такт. Вы можете считать задержку в один такт эквивалентом загрузки еще трех мопов в RAT. С вероятностью в 1/3 что три мопа пойдут в RAT вместе, средняя задержка будет эквивалентна 3/3 = 1 моп. Чтобы посчитать следнее время, которое займет для некоторого кода проход через RAT, добавьте количество потенциальных задержек чтения регистров к количеству мопов и поделите на три. Вы можете видеть, что нет смысла убирать задержки с помощью добавления дополнительных регистров, пока вы точно не уверены, какие мопы пойдут в RAT вместе, или же вы можете предотвратить больше, чем одну задержку чтения регистра с помощью одной дополнительной инструкции.
В ситуациях, когда вашей целью является производительность 3 мопа за такт, ограничение в чтении только двух постоянных регистров за такт может стать узким местом, которое будет трудно обойти. Возможные пути обойти возможные задержки чтения регистра следующие:
Для инструкций, которые генерируют больше одного мопа, вы можете захотить узнать порядок мопов, генерируемых инструкцией, чтобы сделать предсказание возможных задержек чтения регистра более точным. Ниже я перечислил наиболее общие случаи.
- Держать мопы, которые читают один и тот же регистр близко друг к другу, чтобы они попали в один триплет.
- Держать мопы, которые читают разные регистры подальше друг от друга, чтобы они не могли попасть в один триплет.
- Располагать мопы, которые читают регистр не дальше трех-четырех триплетов от инструкции, которая писала или изменяла этот регистр, чтобы он не был выгружен прежде, чем будут произведены все необходимые операции чтения (не важно, если у вас есть переход, если он будет правильно предсказан). Если у вас есть основания полагать, что запись в регистр будет задержена, вы можете спокойно читать из него еще некоторое количество инструкций.
- Использовать абсолютные адреса вместо указателей, чтобы снизить количество читаемых регистров.
- Вы можете переименовать регистр в триплете, если это не вызывает задержку, чтобы предотвратить задержку чтения для этого регистра в одном или больше следующих далее триплетов. Пример: 'MOV ESP,ESP / ... / MOV EAX,[ESP+8]'. Этот метод стоит дополнительный моп, поэтому его стоит применять только, если среднее предполагаемое количество предотвращенных задержек чтения больше 1/3.
Запись в память
Запись в память генерирует два мопа. Первый (в порт 4 ) - это операция сохранения, считывающая регистр, который нужно сохранить, второй моп вычисляет адрес памяти, читая любые регистры-указатели.
Примеры:
Код (Text):
MOV [EDI], EAXПервый моп читает EAX, второй читает EDI.
Код (Text):
FSTP QWORD PTR [EBX+8*ECX]Первый моп читает ST(0), второй моп читает EBX и ECX.
Чтение и модифицирование
Инструкция, которая читает операнд в памяти и модифицирует регистр с помощью какой-либо арифметической или логической операции генерирует два мопа. Первый (порт 2) - это инструкция загрузки из памяти, читающая любой регистр-указатель, второй моп - это арифметическая инструкция (порт 1 или 2), читающая и пишущая в регистр назначения и, возможно, пишущая в регистр флагов.
Пример:
Код (Text):
ADD EAX, [ESI+20]Первый моп читает ESI, второй моп читает EAX и пишет EAX и регистр флагов.
Чтение/модифицирование/запись
Инструкция этого рода генерирует четыре мопа. Первый моп (порт 2) читает регистр-указатель, второй (порт 0 или 1) читает и производит запись в исходный регистр и, возможно, пишет в регистр флагов, третий моп (порт 4) считывает только временный результат, которые не учитывается здесь, четвертый (порт 3) читает все регистры-указатели снова. Так как первый и четвертый регистр не могут идти в RAT вместе, вы не получить преимущество от того, что они читают один и тот же регистр-указатель.
Пример:
Код (Text):
OR [ESI+EDI], EAXПервый моп читает ESI и EDI, второй читает EAX и пишет в EAX и в регистр флагов, третий читает только временный результат, четвертый читает ESI и EDI снова. Вне зависимости о того, в каком порядке эти мопы пойдут в RAT, вы можете быть уверены, что моп, которы читает EAX пойдет вместе с тем, который читает ESI и EDI. Поэтому задержка чтения регистра неизбежна в этой инструкции, если только один из регистров не был изменен ранее.
Сохранение регистра в стек.
Сохранение регистра в стек генерирует 3 мопа. Первый (порт 4) - это инструкция сохранения, чтение регистра. Второй моп (порт 4) генерирует адрес, считывая указатель на стек. Третий (порт 0 или 1) вычитает размер слова из указателя на стек, читая и модифицируя его.
Обратная операция генерирует два мопа. Первый (порт 2) загружает значение, считывая указатель на стек и записывая значение в регистр. Второй моп (порт 0 или 1) корректирует указатель на стек, читая и модифицируя его.
Вызов
Ближний вызов генерирует 4 мопа (порты 1, 4, 3, 01). Первый два мопа читают указатель на инструкцию (EIP), который не учитывается, потому что он не может быть переименован. Третий моп читает указатель на стек. Последний моп читает и модифицирует его.
Возврат
Ближний возврат генерирует 4 мопа (порт 2, 01, 01, 1). Первый моп считывает ESP. Третий читает и модифицирует его.
Пример того, как избежать задержку чтения регистра можно найти в примере 2.6. © Агнер Фог, пер. Aquila
Оптимизация для процессоров семейства Pentium: 16. Переименование регистров (PPro, PII и PIII)
Дата публикации 22 авг 2002