Оптимизация для процессоров семейства Pentium: 15. Доставка инструкций (PPro, PII и PIII)

Дата публикации 22 авг 2002

Оптимизация для процессоров семейства Pentium: 15. Доставка инструкций (PPro, PII и PIII) — Архив WASM.RU

Код доставляется в двойной буфер из кэша кода чанками по 16 байт. Двойной буфер называется так, потому что он содержит два таких чанка. Затем код берется из двойного буфера и скармливается декодерам поблочно (каждый блок обычно 16 байтов длиной, но необязательно, он может быть и не выравнен по этой границе). Я называю эти блоки БДИ (блоки доставки инструкций). Если БДИ пересекает границу 16 байт в коде, его нужно изъять из обоих чанко и поместить в буфер. Таким образом, двойной буфер нужен для того, чтобы позволить доставку инструкций, пересекающих барьер в 16 байт.

Двойной буфер может доставить один 16-ти байтный чанк за такт и может сгенерировать один БДИ за это же время. БДИ обычно 16-ти байтов длиной, но могут быть короче, если есть предсказанный переход в блоке. (Смотри главу 22 о предсказании переходов).

К сожалению, двойной буфер недостаточно велик, чтобы обрабатывать инструкции связанные с переходами без задержек. Если БДИ, который содержит инструкцию перехода, пересекает 16-ти байтную границу, двойному буферу требуется держать два последовательных 16-ти байтных чанка, чтобы сгенерировать его. Если первая инструкция после перехода пересекает 16-ти байтную границу, тогда двойной буфер должен загрузить два новых 16-ти битных чанка кода, прежде чем будет сгенерировать БДИ. Это ознаает, что в худшем случае раскодировка первого инструкции после перехода может быть задержана на два цикла. У вас происходят потери из-за 16-ти байтных границ в БДИ, содержащем переход, и из-за пересечения 16-ти байтной границы после перехода. Вы можете получить выигрыш в производительности, если у вас больше, чем одна раскодировываемая группа в БДИ, которая содержит переход, потому что это дает двойному буферу дополнительное время для доставки одного или двух чанков кода, следующих за переходом. Подобные выигрыши могут компенсировать потери согласно таблице ниже. Если двойной буфер доставляет только один 16-ти байтный чан после перехода, тогда первый БДИ после перехода будет идентичен этом чанку, то есть выравнен по 16-ти байтной границе. Другими словами, первый БДИ после перехода не будет начинаться с первой инструкции, но с ближайшего предшествующего адреса, кратного 16. Если у двойного буфера есть время, чтобы загрузит два чанка, тогда новый БДИ может пересечь 16-ти байтную границу и начаться с первой инструкции после перехода. Эти правила кратко прорезюмированы в следующей таблице:

Код (Text):
  1.  
  2.  Количество     16-байтная      16-байтная       задержка выравнивание
  3.  декодируемых   граница в этом  граница в первой декодера первого БДИ
  4.  груп в БДИ,    БДИ             инструкции после          после перехода
  5.  содержащем                     перехода
  6.  переход
  7.         1              0               0            0           на 16
  8.         1              0               1            1      к инструкции
  9.         1              1               0            1           на 16
  10.         1              1               1            2      к инструкции
  11.         2              0               0            0      к инструкции
  12.         2              0               1            0      к инструкции
  13.         2              1               0            0           на 16
  14.         2              1               1            1      к инструкции
  15.     3 или больше       0               0            0      к инструкции
  16.     3 или больше       0               1            0      к инструкции
  17.     3 или больше       1               0            0      к инструкции
  18.     3 или больше       1               1            0      к инструкции

Переходы задерживают доставку, поэтому цикл всегда занимает на два такта больше за выполнение, чем количество 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.
Я уверен, что теперь вы хотите получить пример:

Код (Text):
  1.  
  2.  адрес        инструкция              длина     мопыы   ожидаемый декодер
  3.  
  4.  ----------------------------------------------------------------------
  5.  1000h        MOV ECX, 1000             5         1       D0
  6.  1005h   LL:  MOV [ESI], EAX            2         2       D0
  7.  1007h        MOV [MEM], 0             10         2       D0
  8.  1011h        LEA EBX, [EAX+200]        6         1       D1
  9.  1017h        MOV BYTE PTR [ESI], 0     3         2       D0
  10.  101Ah        BSR EDX, EAX              3         2       D0
  11.  101Dh        MOV BYTE PTR [ESI+1],0    4         2       D0
  12.  1021h        DEC ECX                   1         1       D1
  13.  1022h        JNZ LL                    2         1       D2

Давайте предположим, что первый БДИ начинается по адресу 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-ти байтовую версию, используя чиловой параметр размером в двойное слово, используя этот трюк:

Код (Text):
  1.  
  2.          SUB ECX, 9999
  3.          ORG $-4
  4.          DD 1

Инструкции с операндами в памяти можно сделать на оди байт длинее с помощью SIB-байта, но самым легким путем сделать инструкцию на один байт длинее является добавление сегментного префикса DS (DB 3Eh). Микропроцессоры обычно принимают избытычные и ничего не значащие префиксы (не считая LOCK), если длина инструкции не превышает 15-ти байт. Даже инструкции без операндов памяти могут иметь сегментный префикс. Поэтому если вы хотите, чтобы инструкция 'DEC ECX' была два байта длиной, напишите:

Код (Text):
  1.  
  2.          DB  3Eh
  3.          DEC ECX

Помните, что при этом у вас будут потери при раскодировке, если у инструкции больше одного префикса. Вомзожно, что инструкции с ничего не значащими префиксами, особенно префиксами повторения и закрытия, будут использоваться в будущих процессорах для новых инструкций, когда не останется свободных кодов, но полагаю, что использование сегментных префиксов с уже существующими инструкциями вполне безопасно. © Агнер Фог, пер. Aquila


0 701
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532