Оптимизация для процессоров семейства Pentium (часть 2)

Дата публикации 25 фев 2017 | Редактировалось 15 апр 2017
10.2 Несовершенное спаривание
  1. Бывают ситуации, когда две спаривающиеся инструкции не будут выполняться одновременно или будут частично рассинхронизированы во времени. Пока обе инструкции не выполняться (каждая в своем конвейере) ни одна другая инструкция не начнет выполняться.
    Несовершенное спаривание возникает в следующих случаях:
    • Если вторая инструкция приводит к задержке AGU (глава 9).
    • Две инструкции не могут обращаться к одному и тому двойному слову в памяти одновременно:
    Код (ASM):
    1. MOV AL, [ESI]
    2. MOV BL, [ESI+1]
    Два операнда внутри одного и того же двойного слова, поэтому они не могут выполняться одновременно. Пара займет два такта для выполнения.
    Код (ASM):
    1. MOV AL, [ESI+3]
    2. MOV BL, [ESI+4]
    Эти два операнда находятся в разных двойных словах, поэтому они прекрасно спариваются и занимают всего один такт.
  2. Правило 2 расширяет зону своего действия, если биты 2-4 обоих адресов одинаковы (конфликт банков кэша). Для адресов размеров в двойное слово это означает, что они не разность между обоими адресами не должна делиться на 32.
    Код (ASM):
    1.  MOV [ESI], EAX
    2. MOV [ESI+32000], EBX ; несовершенное спаривание
    Код (ASM):
    1. MOV [ESI], EAX
    2. MOV [ESI+32004], EBX ; совершенное спаривание
    Спариваемые целочисленные инструкции, которые не обращаются к памяти, требуют один такт для выполнения, кроме неправильно предсказанных переходов. Инструкции MOV, читающие или пишущие в память также занимают один такт, если данные находятся в кэше и правильно выровнены. Нет потери в скорости при использовании сложных способов адресации, такие как смещение и масштабирование.
    Спариваемая целочисленная инструкция, которая читает из памяти, делает какие-то вычисления, а затем сохраняет результат в регистрах или флагах, занимает 2 такта (инструкции чтения/модифицирования).
    Спариваемая целочисленная инструкция, которая читает из памяти, делает какие-то вычисления, а затем записывает результат обратно в память, занимает 3 такта (инструкции чтения/модифицирования/записи).
  3. Если инструкция чтения/модифицирования/записи спаривается с инструкцией чтения/модифицирования или чтения/модифицирования/записи, тогда они спариваются несовершенно.
    Количество тактов, которые потребуются для выполнения такой пары, даны в следующей таблице:
    Первая инструкция
    Вторая
    инструкция
    MOV или регистр
    чтение/изменение
    чтение/изменение/запись
    MOV или регистр123
    чтение/изменение223
    чтение/изменение/запись345
    Код (ASM):
    1. ADD [mem1], EAX
    2. ADD EBX, [mem2] ; 4 такта ADD EBX, [mem2]
    3. ADD [mem1], EAX ; 3 такта
    Когда две спаривающиеся инструкции требуют дополнительное время для выполнения из-за неоптимального использования кэша, невыровнености или неправильно предсказанного условного перехода, они будут выполнятся дольше, чем каждая инструкция, но меньше, чем сумма времени, требующаяся на выполнение каждой из них по отдельности.
  4. Спариваемая инструкция плавающей запятой и следующая за ней FXCH повлекут несовершенное спаривание, если следующая инструкция не является инструкцией плавающей запятой.
    Чтобы избежать несовершенного спаривания, вы должны знать, какие инструкции пойдут в U-конвейер, а какие — в V-конвейер. Вы можете выяснить это, просмотрев свой код и поискав инструкции, которые не спариваются, или спариваются только в определенном конвейере, или не могут спариваться в силу одного из вышеизложенных правил.
    Несовершенное спаривание можно зачастую избежать, изменив порядок следования инструкций.
    Код (ASM):
    1. L1: MOV EAX,[ESI]
    2.  MOV EBX,[ESI]
    3.  INC ECX
    Здесь две инструкции MOV формируют несовершенную пару, потому что обе обращаются к одной и той же области в памяти, поэтому последовательность займет 3 такта. Вы можете улучшить этот код, переставив инструкции так, чтобы 'INC ECX' спаривалась с одной из инструкции MOV.
    Код (ASM):
    1. L2: MOV EAX,OFFSET A
    2.  XOR EBX,EBX
    3.  INC EBX
    4.  MOV ECX,[EAX]
    5.  JMP L1
    Пара 'INC EBX / MOV ECX,[EAX]' несовершенная, потому что следующая инструкция приводит к задержке AGI. Последовательность занимает 4 такта. Если вы вставите NOP или какую-нибудь другую инструкцию, чтобы 'MOV ECX,[EAX]' спаривался с 'JMP L1', последовательность займет только три такта.
    Следующий пример выполняется в 16-ти битном режима, предполагается,что SP делится на 4:
    Код (ASM):
    1. L3: PUSH AX
    2.  PUSH BX
    3.  PUSH CX
    4.  PUSH DX
    5.  CALL FUNC
    Инструкции PUSH формируют две несовершенные пары, потому что оба операнда в каждой паре обращаются к одному и тому же слову в памяти. 'PUSH BX' могла бы совершенно спариваться с PUSH CX (потому что они находятся по разные стороны от границы, отделяющей двойные слова друг от друга), но этого не происходит, потому что она уже спарена с PUSH AX. Поэтому последовательность занимает 5 тактов. Если вы вставите NOP или другую инструкцию, чтобы 'PUSH BX' спаривалась с 'PUSH CX', а 'PUSH DX' с 'CALL FUNC', последовательность займет только 3 такта. Другой путь разрешения данной проблемы — это убедиться, что SP не кратен четырем. Правда, узнать это в 16-ти разрядном режиме довольно сложно, поэтому лучший выход — использовать 32-х разрядный режим.
11. Разбивка сложных инструкций на более простые (PPlain и PMMX)
Вы можете разбить инструкции чтения/модифицирования и инструкции чтения/модифицирования/записи, чтобы улучшить спаривание.
Код (ASM):
  1. ADD [mem1],EAX
  2. ADD [mem2],EBX ; 5 тактов
Этот код можно разбить на следующую последовательность, которая будет занимать только 3 такта:
Код (ASM):
  1.     MOV ECX,[mem1]
  2.     MOV EDX,[mem2]
  3.     ADD ECX,EAX
  4.     ADD EDX,EBX
  5.     MOV [mem1],ECX
  6.     MOV [mem2],EDX
Таким же образом вы можете разбивать не спариваемые инструкции на спариваемые:
Код (ASM):
  1.     PUSH [mem1]
  2.     PUSH [mem2]  ; не спаривается
Разбивается на:
Код (ASM):
  1.     MOV EAX,[mem1]
  2.     MOV EBX,[mem2]
  3.     PUSH EAX
  4.     PUSH EBX     ; все спаривается
Другие примеры неспариваемых инструкций, которые можно разбить на простые спариваемые:
  • CDQ разбивается на MOV EDX,EAX / SAR EDX,31
  • NOT EAX меняется на XOR EAX,-1
  • NEG EAX разбивается на XOR EAX,-1 / INC EAX
  • MOVZX EAX,BYTE PTR [mem] разбивается на XOR EAX,EAX / MOV AL,BYTE PTR [mem]
  • JECXZ разбивается на TEST ECX,ECX / JZ
  • LOOP разбивается на DEC ECX / JNZ
  • XLAT меняется на MOV AL,[EBX+EAX]
Если разбивание инструкций не увеличивает скорость выполнения, вы можете оставить сложные или неспариваемые конструкции, чтобы уменьшить размер кода.
Разбивание инструкций не требуется, кроме тех случаев, когда это будет генерировать меньше микроопераций (англ. uops).
12. Префиксы (PPlain и PMMX)
Инструкция с одним или более префиксами не может исполняться в V-конвейере (смотри главу 10, секцию 7).
На PPlain возникает задержка в один такт для каждого префикса, кроме 0Fh в инструкциях условного ближнего перехода.
У PMMX не задержки раскодирования из-за префикса 0Fh. Префиксы сегментов и повторения занимают один такт для раскодирования. PMMX может раскодировать две инструкции, если у первой инструкции нет префиксов или есть префикс сегмента или повторения, у второй инструкции нет префиксов. Инструкции с префиксами размера адреса или операнда на PMMX раскодировываются только отдельно. Инструкции с несколькими префиксами занимают по одному такту на каждый префикс.
Префиксы размера адреса нужно избегать, используя 32-х битный режим. Префиксы сегментов можно избежать в 32-х битном режиме, используя плоскую модель памяти. Префикс размера операнда можно избежать в 32-х битном режиме, используя только 8-ми битные и 32-х битные числа.
Когда префиксы нельзя избежать, задержку раскодирования можно скрыть, если предыдущая инструкция занимает больше одного такта для исполнения. Правило для PPlain такого: каждая инструкция, которая занимает N тактов для выполнения (не для раскодирования) может 'затенять' задержку раскодирования N-1 префиксов в следующих двух (иногда трех) инструкциях или пар инструкций. Другими словами, каждый дополнительных так, который требует инструкция для выполнения, может быть использован для раскодирования одного префикса в следующей инструкции. Этот эффект иногда распространяется даже на правильно предсказанный переход. Любая инструкция, которая занимает больше одного такта для выполнения, и любая инструкция, чье выполнение задерживается из-за AGI, неправильного использования кэша, неправильного выравнивания или неправильного предсказания перехода, создает эффект 'затемнения'.
У PMMX есть похожий эффект 'затемнения', но он имеет другой механизм. Раскодировываемые инструкции хранятся в прозрачном "первым-вошел-первым-вышел" (FIFO) буфере, в котором может храниться до четырех инструкций. Когда буфер пуст, инструкции выполняются сразу после раскодировки. Бфер заполняется, когда инструкции раскодировываются быстрее, чем выполняются, то есть они неспарены или являются мультитактовыми. Буфер FIFO опустошается, когда инструкции выполняются быстрее, чем раскодировываются, то есть когда возникают задержки из-за префиксов. Буфер FIFO становится пустым после неправильно предсказанного перехода. Буфер FIFO может получить две инструкции за такт, если у второй инструкции нет префиксов и ни одна из инструкций не длиннее 7 байт. Два конвейера (U и V) могут получать по одной инструкции за такт из буфера FIFO.
Код (ASM):
  1. CLD / REP MOVSD
Инструкция CLD занимает два такта, и поэтому затемняет задержку раскодировки префикса REP. Код занял бы на один такт больше, если бы инструкция CLD находилась достаточно далеко от REP MOVSD.
Код (ASM):
  1.    CMP DWORD PTR [EBX],0
  2.  MOV EAX,0
  3.  SETNZ AL
Инструкция CMP занимает два такта, потому что это инструкция чтения/модифицирования. Префикс 0Fh инструкции SETNZ раскодировывается во время второго такта выполнения CMP, поэтому задержка раскодирования скрыта на PPlain (у PMMX нет задержки для 0Fh).
Потери производительности, связанные с префиксами, на PPro, PII и PIII объясняются в главе 14.
13. Обзор конвейера PPro, PII и PIII
Архитектура микропроцессоров PPro, PII и PIII хорошо объяснена и проиллюстрирована в различных руководствах от Intel'а. Рекомендуется, сначала изучить этот материал, чтобы понимать, как работают эти процессоры. Я коротко объясню его структуру с упором на те элементы, которые необходимы для оптимизирования кода.
Коды доставляются из кэша кода выравненными 16-ти байтными чанками в двойной буфер, в который умещается два таких чанка. Затем код поступает из двойного буфера в декодеры в виде блоков, названными мной блоками доставки инструкций (БДИ). Обычно длина БДИ 16 байт, но он может быть не выравнен. Цель двойного буфера — сделать возможным раскодировку инструкций, которые пересекают границу в 16 байт (например адрес, кратный 16).
БДИ поступает в декодер длины инструкции, который определяет, где начинается каждая из инструкций и где заканчивается, а затем попадает в декодеры инструкций. Есть три декодера, поэтому вы можете декодировать до трех инструкций за такт. Группа из трех инструкций, декодируемых за один и тот же такт, называется декодируемой группой.
Декодеры переводят инструкции в микрооперации (сокращенно "мопы"). Простые инструкции генерируют только одну микрооперацию, в то время, как более сложные инструкции могут генерировать несколько микроопераций. Например, инструкция 'ADD EAX,[MEM]' декодируется в две микрооперации, одна, считывает исходный операнд из памяти, а другая выполняет сложение. Целью разбития инструкций на микрооперации является сделать обработку инструкций более эффективной.
Три декодера называются D0, D1 и D2. D0 может обрабатывать все инструкции, в то время как D1 и D2 может обрабатывать только простые инструкции, генерирующие только одну микрооперацию.
Микрооперации из декодеров через короткую очередь поступают в таблицу распределения регистров (register allocation table — RAT). Выполнение микроопераций осуществляется на временных регистрах, после чего результат записывается в постоянные регистры EAX, EBX и так далее. Целью RAT является указание микрооперациям, какие временные регистры использовать и позволить переименование регистров (смотри далее).
После RAT микрооперации поступают в буфер перегруппировки (ROB). Целью ROB является неупорядоченное выполнение. Микрооперации остаются в области рекреации (reservation station), пока не станут доступны операнды, которые им необходимы. Если операнд для микрооперации задерживается из-за того, что генерирующий его предыдущая микрооперация еще не закончил свою работу, тогда ROB может найти другая микрооперация в очереди, чтобы сэкономить время.
Готовые к выполнению микрооперации посылаются в модули выполнения (execution units), которые сгруппированы вокруг пяти портов: порт 0 и 1 могут обрабатывать все арифметические операции, переходы и так далее. Порт 2 берет на себя операции считывания из памяти, порт 3 высчитывает адреса для записи в память, а порт 4 выполняет эту запись.
После того, как инструкция была выполнена, она помечается в ROB как готовая к удалению, после чего поступает в область удаления (retirement station). Здесь содержимое временных регистров, использованных микрооперациями, записывается в постоянные регистры. Хотя микрооперации могут запускаться не по порядку, последний должен быть восстановлен при удалении.
14. Раскодировка инструкций (PPro, PII и PIII)
Я рассказываю о раскодировке инструкций до доставки инструкций, потому что вам необходимо знать, как работают раскодировщики, чтобы понимать возможные способы доставки.
Декодеры могут обрабатывать три инструкции за такт, но только, если соблюдены определенные условия. Декодер D0 может обработать за один такт любую инструкцию, которая генерирует до 4 мопов. Декодеры D1 и D2 могут обрабатывать только те инструкции, которые генерируют 1 моп и эти инструкции не могут занимать больше 8 байт.
Резюмируем правила для декодирования двух или трех инструкций за один такт:
  • Первая инструкция (D0) не должна генерировать больше 4-х мопов.
  • Вторая и третья инструкции не должны генерировать больше, чем по одному мопу.
  • Вторая и третья инструкции не могут быть занимать больше 8-ми байтов каждая.
  • Инструкции должны содержаться внутри одного 16-байтного БДИ (смотри следующую главу).
Нет ограничение на длину инструкции в D0 (несмотря на то, то руководства от Интел говорят об обратном) пока все три инструкции влезают в один 16-ти байтный блок.
Инструкция, которая генерирует больше 4-х мопов требует два или больше такта для раскодировки, и ни одна другая инструкция не может раскодировываться параллельно.
Из вышеприведенных правил следует, что декодеры могут генерировать максимум шесть мопов за такт, если первая инструкция в каждой раскодировываемой группе разбивается на 4 мопа, а другие две - на один каждая. Минимальное количество - это два мопа за такт, если все инструкции генерируют по два мопа, так что D1 и D2 никогда не используются.
Для максимальной производительности рекомендуется перегруппировать ваши инструкции в блоки 4-1-1: инструкции, которые генерирует 2 или 4 мопа можно разбить на две простые одномопные инструкции, что не будет вам стоить ни такта.
Код (ASM):
  1. MOV     EBX, [MEM1]     ; 1 uop  (D0)
  2. INC     EBX             ; 1 uop  (D1)
  3. ADD     EAX, [MEM2]     ; 2 uops (D0)
  4. ADD     [MEM3], EAX     ; 4 uops (D0)
Это занимает три такта для раскодировки. Вы можете сохранить один такт, перегруппировав инструкции в две декодируемые группы:
Код (ASM):
  1. ADD     EAX, [MEM2]     ; 2 uops (D0)
  2.    MOV     EBX, [MEM1]     ; 1 uop  (D1)
  3.    INC     EBX             ; 1 uop  (D2)
  4.    ADD     [MEM3], EAX     ; 4 uops (D0)
Теперь декодеры генерируют 8 мопов за два такта, что удовлетворительно. На более поздних стадиях конвеер может обрабатывать только 3 мопа за такт, поэтому при скорости декодирования высшей, чем эта, вы можете считать, что декодирование не является узким местом. Тем не менее, сложности в механизме доставки могут задержать раскодирование так, как это рассказывается в следующей главе, поэтому чтобы быть спокойным, лучше стремиться к скорости раскодировки большей 3-х мопов за такт.
Вы можете узнать количество генерируемых инструкцией мопов в таблице в главе 29.
Префиксы инструкций также приводят к потере скорости раскодировки. У инструкций могут быть префиксы следующих видов:
Префикс размера операнда требуется, когда вы используете 16-ти битный операнд в 32-х битном окружении или наоборот. (Не считая инструкций, у которых операнды могут быть только одного размера, например FNSTSW AX). Префикс размера операнда вызывает потерю нескольких тактов, если у инструкции есть числовой 16-ти или 32-х битный операнд, потому что размер операнда меняется префиксом.
Код (ASM):
  1.         ADD BX, 9      ; нет потерь, так как числовой операнд занимает 8 бит
  2.         MOV WORD PTR [MEM16], 9  ; есть потери, так как операнд занимает 16 бит
Последнюю инструкцию следует заменить на
Код (ASM):
  1.         MOV EAX, 9
  2.         MOV WORD PTR [MEM16], AX  ; нет потерь, так как нет числовых операндов
  • Префикс размера адреса используется при 32-х битной адресации в 16-ти битном режиме или наоборот. Это редко требуется и этого следует избегать. Префикс размера адреса вызывает потери каждый раз, когда операнды используются явно, потому что интерпретация изменяется с помощью префикса. Инструкции, использующие операнды памяти неявно (например строковые операции), не приводят к потерям, связанных с префиксом размера операнда.
  • Префиксы сегментов используются, когда вы обращаетесь к данным, находящимся не в сегменте данных по умолчанию. На PPro, PII и PIII префиксы сегментов не приводят к потерям.
  • Префиксы повторения и префиксы закрытия (lock prefixes) не приводят к потерям при декодировании.
  • Всегда есть потери, когда у вас больше одного префикса. Обычно уходит по одному такту на префикс.
15. Доставка инструкций (PPro, PII и PIII)
Код доставляется в двойной буфер из кэша кода чанками по 16 байт. Двойной буфер называется так, потому что он содержит два таких чанка. Затем код берется из двойного буфера и скармливается декодерам по-блочно (каждый блок обычно 16 байтов длиной, но необязательно, он может быть и не выравнен по этой границе). Я называю эти блоки БДИ (блоки доставки инструкций). Если БДИ пересекает границу 16 байт в коде, его нужно изъять из обоих чанков и поместить в буфер. Таким образом, двойной буфер нужен для того, чтобы позволить доставку инструкций, пересекающих барьер в 16 байт.
Двойной буфер может доставить один 16-ти байтный чанк за такт и может сгенерировать один БДИ за это же время. БДИ обычно 16-ти байтов длиной, но могут быть короче, если есть предсказанный переход в блоке. (Смотри главу 22 о предсказании переходов).
К сожалению, двойной буфер недостаточно велик, чтобы обрабатывать инструкции связанные с переходами без задержек. Если БДИ, который содержит инструкцию перехода, пересекает 16-ти байтную границу, двойному буферу требуется держать два последовательных 16-ти байтных чанка, чтобы сгенерировать его. Если первая инструкция после перехода пересекает 16-ти байтную границу, тогда двойной буфер должен загрузить два новых 16-ти битных чанка кода, прежде чем будет сгенерировать БДИ. Это означает, что в худшем случае раскодировка первого инструкции после перехода может быть задержана на два цикла. У вас происходят потери из-за 16-ти байтных границ в БДИ, содержащем переход, и из-за пересечения 16-ти байтной границы после перехода. Вы можете получить выигрыш в производительности, если у вас больше, чем одна раскодировываемая группа в БДИ, которая содержит переход, потому что это дает двойному буферу дополнительное время для доставки одного или двух чанков кода, следующих за переходом. Подобные выигрыши могут компенсировать потери согласно таблице ниже. Если двойной буфер доставляет только один 16-ти байтный чан после перехода, тогда первый БДИ после перехода будет идентичен этом чанку, то есть выравнен по 16-ти байтной границе. Другими словами, первый БДИ после перехода не будет начинаться с первой инструкции, но с ближайшего предшествующего адреса, кратного 16. Если у двойного буфера есть время, чтобы загрузит два чанка, тогда новый БДИ может пересечь 16-ти байтную границу и начаться с первой инструкции после перехода. Эти правила кратко прорезюмированы в следующей таблице:
Количество
декодируемых
групп в БДИ,
содержащем
переход
16-байтная
граница
в этом
БДИ
16-байтная
граница
в первой
инструкции
после
перехода
задержка
декодера
выравнивание
первого БДИ
после переход
1000на 16
1011к инструкции
1101на 16
1112к инструкции
2000к инструкции
2010к инструкции
2100на 16
2111к инструкции
3 или больше000к инструкции
3 или больше010к инструкции
3 или больше100к инструкции
3 или больше110к инструкции
Переходы задерживают доставку, поэтому цикл всегда занимает на два такта больше за выполнение, чем количество 16-ти байтных границ в цикле.
Следующая проблема с механизмом доставки инструкций заключается в том, что новый БДИ не сгененирируется, пока предыдущий не будет полностью отработан. Каждый БДИ может содержать несколько раскодировываемых групп. Если БДИ длиной 16 байт заканчивается незавершенной инструкцией, тогда следующий БДИ начнется в начале этой инструкции. Первая инструкция в БДИ всегда идет в D0, а следующие две инструкции направляются в D1 и D2, если это возможно. Как следствие, D1 и D2 используются не совсем оптимально. Если код структурирован согласно правилу 4-1-1, а инструкция, которая, как предполагалось, должна направиться в D1 или D2, оказывается первой инструкцией в БДИ, тогда она попадает в D0, что ведет к потере одного такта. Вероятно, это недостаток архитектуры процессора. Из-за этого время, которое займет раскодировка определенного кода может зависеть от того, где начнется первый БДИ.
Если скорость раскодирования инструкций критична, и вы хотите избежать этой проблемы, вам нужно знать, где начинается каждый БДИ. Это довольно нудная работа. Вначале вам разделить ваш сегмент на параграфы, чтобы знать, где находятся 16-ти байтные границы. Затем вам нужно взглянуть на ассемблерный листинг, чтобы увидеть, какова длина каждой инструкции. (Рекомендуется, изучить, как кодируются инструкции, чтобы уметь предсказывать их длину.) Если вы знаете, где начинается один БДИ, тогда вы можете найти, где начинается другой следующим образом: сделать блок 16 байтов длиной. Если он кончается на границе между инструкциями, значить следующий БДИ начнется здесь. Если он включает в себя часть инструкции, тогда следующий БДИ начнется с этой инструкции. (Здесь нужно подсчитывать только длины инструкций, не имеет значения, сколько мопов они генерируют.) Таким образом вы можете обработать весь код и отметить, где начинается каждый из БДИ-блоков. Единственная проблема - это узнать, где находится первый БДИ. Вот несколько подсказок.
Первый БДИ после перехода, вызова или возвращения может начинаться либо с первой, либо с ближайшей предшествующей 16-ти байтной границе, согласно вышеприведенной таблице. Если вы выравняете первую инструкцию так, чтобы она начиналась с 16-байтной границы, вы можете быть уверены, что первый БДИ начнется здесь. Вы можете выравнять подобным образом важные процедур и циклы, чтобы убыстрить работу программы.
Если комбинированная длина двух последовательных инструкций больше 16 байтов, вы можете быть уверены, что вторая не влезет в тот же БДИ, что и первая, следовательно вы можете быть уверены, что вторая инструкция будет первой в БДИ. Вы можете использовать ее в качестве стартовой точки для того, чтобы найти где начинаются следующие БДИ.
Первый БДИ после неправильного предсказания перехода начинается по 16-ти байтной границе. Как объясняется в главе 22.2, цикл, который повторяется больше 5 раз, всегда будет приводить к неправильному предсказанию перехода при выходе. Первый БДИ после такого цикла будет начинаться по ближайшей предшествующей 16-ти байтной границе.
Есть также другие события, приводящие к подобному эффекту, например прерывания, исключения, самомодифирующийся код и такие инструкции как CPUID, IN и OUT.
Я уверен, что теперь вы хотите получить пример:
адресинструкциядлинамопыожидаемый
декодер
1000h MOV ECX, 100051D0
1005hLL: MOV [ESI], EAX22D0
1007h MOV [MEM], 0102D0
1011h LEA EBX, [EAX+200]61D1
1017h MOV BYTE PTR [ESI], 032D0
101Ah BSR EDX, EAX32D0
101Dh MOV BYTE PTR [ESI+1],042D0
1021h DEC ECX11D1
1022h JNZ LL21D2
Давайте предположим, что первый БДИ начинается по адресу 1000h и заканчивается в 1010h. Это до завершения инструкции 'MOV [MEM], 0', поэтому следующий БДИ начнется в 1007h и закончится в 1017h. Это граница между инструкциями, поэтому третий БДИ начнется в 1017h и захватит весь остаток цикла. Количество тактов, которое уйдет на раскодировку - это количество инструкций, попадающих в D0. Всего их в цикле 5. Последний БДИ содержит три раскодировываемых блоков, включая последние пять инструкций, и одну 16-ти байтную границу (1020h). Если мы еще раз посмотрим на таблицу, то увидим, что первый БДИ после перехода будет начинаться с первой инструкции после перехода (та, где стоит метка LL) и заканчиваться у 1015h. Это перед концом инструкции LEA, поэтому следующий БДИ будет начинаться с 1011h до 1021h, а последний будет идти от 1021h до самого конца. Теперь LEA и DEC попадают в начало БДИ, поэтому они обе направляются в D0. У нас есть 7 инструкции в D0 и на раскодироваку цикла во втором повторении уходит 7 тактов. Последний БДИ содержит только одну раскодировываемую группу (DEC ECX / JNZ LL) и у него нет 16-ти байтной границы. Согласно таблице, следующий БДИ после перехода начнется с 16-ти байтной границы, то есть 1000h. Мы оказываемся в той же ситуации, что и в первом повторении, и вы увидите, что раскодировка цикла занимает соответственно 5 и 7 тактов. Так как других узких мест нет, на выполнение цикла 1000 раз уйдет 6000 тактов. Если бы стартовый адрес был другим, и в первой или последней инструкции была бы 16-ти байтная граница, то это бы заняло 8000 тактов. Если вы перегруппируете цикл, так чтобы инструкции для D1 или D2 не попадали в начало БДИ, тогда выполнение цикла заняло бы еще меньше - 5000 тактов.
Вышеприведенный пример был преднамеренно построен так, что единственным узким местом является доставка инструкций и их раскодировка. Самый легкий путь избежать этой проблемы - это структурировать ваш код, чтобы он генерировал больше 3 мопов за такт, чтобы раскодировка не была узким местом несмотря на приведенные потери скорости. В маленьких циклах это может быть невозможно, поэтому вам нужно найти путь, чтобы оптимизировать доставку инструкций и раскодировку.
Например вы можете изменить стартовый адрес вашей процедуры, чтобы избежать 16-ти байтных границ, когда они вам не нужны. Помните, ваш сегмент кода должен быть выравнен по параграфу, чтобы вы знали, где находятся границы.
Если вы вставите директиву 'ALIGN 16' до начала цикла, тогда ассемблер поместит NOP и другие подобные инструкции, чтобы выравнять код. Большинство ассемблеров используют инструкцию 'XCNG EBX, EBX' в качестве двухбайтного заполнителя (иногда ее называют "двухбайтным NOP'ом"). Кому бы ни пришла в голову эта идея, лучше данную инструкцию не использовать, потому что она занимает больше времени, чем два NOP'а на большинстве процессоров. Если цикл выполняется много раз, тогда то, что находится за его пределами неважно, если говорить о скорости и вы не должны заботиться о том, какую инструкцию использовать в качестве заполнителя. Но если время, занимаемое этими инструкциями играет роль, тогда вы можете выбрать инструкцию-заполнитель самостоятельно. Вы также можете использовать инструкции, которые делают что-нибудь полезное, например обновляют регистр, дабы избежать задержек при чтении регистра. Например, если вы используете регистр EBP для адресации, но редко пишете в его, вы можете использовать 'MOV EBP,EBP' или 'ADD EBP, 0' в качестве заполнителя, чтобы снизить возможность вышеупомянутых задержек. Если вам не делать ничего подобного, вы можете использовать FXCH ST(0), потому что она не создает никакой нагрузки на порты выполнения, при условии, что ST(0) содержит верное значение с плавающей запятой.
Еще одним лекарством может стать перегруппировка ваших инструкций таким образом, что границы между БДИ не приносили вреда. Это может стать сложной головоломкой и найти приемлемое решение не всегда возможно.
Другая возможность - манипуляции с длинами инструкций. Иногда вы можете заменить одну инструкцию другой с иной длиной. Многие инструкции можно закодировать различными вариантами с разной длиной. Ассемблер всегда выбирает наиболее короткую версию инструкции, но все можно закодировать более длинную версию. Например 'DEC ECX' занимает один байт длиной, 'SUB ECX, 1' - тир байта, и вы можете закодировать 6-ти байтовую версию, используя числовой параметр размером в двойное слово, используя этот трюк:
Код (ASM):
  1.          SUB ECX, 9999
  2.          ORG $-4
  3.          DD 1
Инструкции с операндами в памяти можно сделать на один байт длиннее с помощью SIB-байта, но самым легким путем сделать инструкцию на один байт длиннее является добавление сегментного префикса DS (db 3Eh). Микропроцессоры обычно принимают избыточные и ничего не значащие префиксы (не считая LOCK), если длина инструкции не превышает 15-ти байт. Даже инструкции без операндов памяти могут иметь сегментный префикс. Поэтому если вы хотите, чтобы инструкция 'DEC ECX' была два байта длиной, напишите:
Код (ASM):
  1.         DB  3Eh
  2.          DEC ECX
Помните, что при этом у вас будут потери при раскодировке, если у инструкции больше одного префикса. Возможно, что инструкции с ничего не значащими префиксами, особенно префиксами повторения и закрытия, будут использоваться в будущих процессорах для новых инструкций, когда не останется свободных кодов, но полагаю, что использование сегментных префиксов с уже существующими инструкциями вполне безопасно.
16. Переименование регистров (PPro, PII и PIII) — Архив WASM.RU
16.1 Уничтожение зависимостей
Переименование регистров - это продвинутая техника, используемая этими микропроцессорами, чтобы убрать зависимости между различными частями кода. Пример:
Код (ASM):
  1.          MOV EAX, [MEM1]
  2.          IMUL EAX, 6
  3.          MOV [MEM2], EAX
  4.          MOV EAX, [MEM3]
  5.          INC EAX
  6.          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 Задержки чтения регистров.
Но существует другое ограничение, которое может быть весьма серьезным, и это то, что за один такт вы можете читать только из двух постоянных регистров. Это ограничение относится ко всем регистрам, используемых инструкциям, не считая тех регистров, в которые инструкции только пишут. Пример:
Код (ASM):
  1.          MOV [EDI + ESI], EAX
  2.          MOV EBX, [ESP + EBP]
Первая инструкция генерирует два мопа: один считывает EAX, а другой - EDI и ESI. Вторая инструкция генерирует один моп, который читает ESP и EBP. EBX не учитывается, поскольку инструкция только пишет в него. Давайте предположим, что эти три мопа идут вместе через RAT. Я буду использовать слово триплет для группы из трех последовательных мопов, которые идут вместе через RAT. Так как ROB может обрабатывать только два чтения из постоянных регистров за такт, а нам нужно пять чтений, то наш триплет будет задержан на два дополнительных такта, прежде чем он попадет в резервационную станцию. При трех или четырех чтений из постоянных регистров он был бы задержан на один такт.
Из одного регистра в одном триплете можно читать больше одного раза без ущерба для качества. Если вышеприведенные инструкции поменять на:
Код (ASM):
  1.          MOV [EDI + ESI], EDI
  2.          MOV EBX, [EDI + EDI]
тогда произойдет только два чтения из регистров (EDI и ESI) и триплет не будет задержан.
Регистр, в который будет произведена запись текущим мопом, сохраняется в ROB, поэтому из него можно свободно читать, пока он не будет выгружен оттуда, что занимает по меньшей мере три такта, а обычно еще больше. Выгрузка из ROB является финальной стадией выполнения, когда значение становится доступным. Другими словами, вы можете читать любое количество раз из регистра в RAT без задержек, если их значение еще не стало доступным у модулей выполнения, поэтому вы можете быть уверены, что из регистра, в который была произведена в одном триплете, можно свободно читать как минимум в трех последующих. Если выгрузка (writeback) была задержана перегруппировкой, медленными инструкциями, цепочками зависимости, задержкой кэша или по какой-то другой причине, тогда из регистра можно свободно читать еще некоторое время.
Код (ASM):
  1.          MOV EAX, EBX
  2.          SUB ECX, EAX
  3.          INC EBX
  4.          MOV EDX, [EAX]
  5.          ADD ESI, EBX
  6.          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.
Ни один моп не может читать больше, чем из двух регистров. Поэтому все инструкции, читающие больше, чем из двух регистров, разбиваются на два или больше мопа.

0 2.006
Mikl___

Mikl___
Супермодератор
Команда форума

Регистрация:
25 июн 2008
Публикаций:
14