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

Дата публикации 25 фев 2017 | Редактировалось 15 апр 2017
Оптимизация для процессоров семейства Pentium — Архив WASM.RU
Содержание
  1. Введение
  2. Литература
  3. Вызов ассемблерных функций из языка высокого уровня
  4. Отладка
  5. Модель памяти
  6. Выравнивание
  7. Кэш
  8. Исполнение кода в первый раз
  9. Задержка генерации адреса
  10. Спаривание целочисленных инструкций (PPlain и PMMX)
  11. Разбивка сложных инструкций на более простые (PPlain и PMMX)
  12. Префиксы (PPlain и PMMX)
  13. Обзор конвейера PPro, PII и PIII
  14. Раскодировка инструкций (PPro, PII и PIII)
  15. Доставка инструкций (PPro, PII и PIII)
  16. Переименование регистров (PPro, PII и PIII)
  17. Выполнение кода не по порядку (PPro, PII и PIII)
  18. Вывод из обращения (PPro, PII и PIII)
  19. Частичные задержки (PPro, PII и PIII)
  20. Цепочки зависимости (PPro, PII и PIII)
  21. Поиск узких мест (PPro, PII и PIII)
  22. Команды передачи управления и переходов (все процессоры)
  23. Уменьшение размера кода (все процессоры)
  24. Работа с плавающей запятой (PPlain и PMMX)
  25. Оптимизация циклов (все процессоры)
  26. Проблемные инструкции
  27. Специальные темы
  28. Список периодов выполнения инструкций для PPlain и PMMX
  29. Список периодов выполнения инструкций и задержек микроопераций для PPro, PII и PIII
  30. Тестирование скорости
  31. Сравнение различных микропроцессоров


1. Введение


Это руководство подробно рассказывает о том, как писать оптимизированный код на ассемблере, с ориентированием на семейство микропроцессоров Pentium.
Большая часть приведенной здесь информации основывается на моих собственных исследованиях. Много людей прислали мне полезную информацию и исправления для этого руководства, и я обновляю его каждый раз, когда у меня появляется важная информация. Поэтому данное руководство наиболее точно, подробно и исчерпывающе, нежели любой другой источник информации, и оно содержит множество подробностей, которые трудно найти где бы то ни было еще. Эта информация во многих случаях позволит вам узнать, сколько тактов займет выполнение определенного кода. Тем не менее, я не утверждаю, что вся информация в данном руководстве верна: некоторые измерения очень трудно или невозможно сделать точно, и у меня нет доступа к информации о внутренней реализации, которая есть у тех, кто пишет Intel'овские руководства.
В данном руководстве обсуждаются следующие версии процессоров Pentium:
аббревиатураимя
PPlainобычный старый Pentium
PMMXPentium с MMX
PProPentium Pro
PIIPentium II (включая Celeron'ы и Xeon'ы)
PIIIPentium III (включая его разновидности)
В данном руководстве используется синтаксис MASM 5.10. Для языка ассемблера x86 не существует официального стандарта, но это фактически является стандартом, так как многие ассемблеры имеют режим совместимости с MASM 5.10. (Я не рекомендую использовать сам MASM версии 5.10, так как в нем есть серьезный баг, проявляющийся в 32-х битном режиме. Используйте TASM или более позднюю версию MASM).
Некоторые замечания в данном руководстве могут показаться критикой Intel. Это не нужно понимать, что другие марки лучше. Семейство микропроцессоров Pentium вполне сравнимы со своими конкурентами, они лучше документированы и у них есть другие отличительные особенности.
Программировать на ассемблере гораздо сложнее, чем на языке высокого уровня. Сделать ошибку очень легко, а найти ее очень трудно. Вы были предупреждены! Предполагается, что у читателя уже есть опыт в программировании на ассемблере. Если нет, пожалуйста, прочитайте соответствующие книги по этому теме и наберитесь определенного опыта в программировании, прежде чем вы начнете выполнять сложную оптимизацию.
Архитектура чипов PPlain и PMMX имеет много особенностей, которые оптимизированы специально под некоторые наиболее часто используемые инструкции или комбинации инструкций. В этих случаях нельзя использовать общие методики оптимизации. Следовательно, правила для оптимизирования программного обеспечения весьма сложны и имеют много исключений, но возможный выигрыш в производительности значителен. У процессоров PPro, PII и PIII совершенно разный дизайн в том, как процессор берет на себя часть оптимизации, запуская инструкции в совершенно разном порядке, но усложненный дизайн этих процессоров становится причиной множества потенциальных узких мест, поэтому можно много выиграть, оптимизируя код вручную для этих процессоров. У Pentium 4 также другой дизайн, и пути оптимизации для Pentium 4 отличаются от тех, которые используются для других версий. Это руководство не рассказывает о Pentium 4 — читатель может обратиться к руководствам от Intel.
Прежде, чем вы начнете конвертировать ваш код в ассемблер, убедитесь, что используемый вами алгоритм оптимален. Зачастую вы можете гораздо сильнее улучшить ваш код путем оптимизации алгоритма, чем переписыванием кода на ассемблере.
Затем вам нужно выделить критические части вашей программы. Зачастую более, чем 99% времени процессора тратится на внутренний цикл программы. В этом случае вам нужно только оптимизировать этот цикл и оставить все остальное на языке высокого уровня. Некоторые асм-программисты тратят огромное количество энергии на оптимизирование не тех частей своей программы, единственным значительным результатом чего становится то, что программу становится труднее отлаживать и распространять!
Если не совсем понятно, какие части вашей программы являются критическим, вы можете использовать профайлер, чтобы найти их. Если выясняется, что узким местом является доступ к диску, вы можете модифицировать вашу программу таким образом, чтобы сделать доступ к диску последовательным (для улучшения работы кэша диска), а не переключаться на ассемблерное программирование. Если узким местом является вывод графики, вы можете поискать путь к уменьшению вызовов графических процедур.
Некоторые высокоуровневые компиляторы предлагают относительно хорошую оптимизацию для некоторых процессоров, но дальнейшая оптимизация вручную обычно дает лучшие результаты.
Пожалуйста, не посылайте мне ваши вопросы о программировании. Я не собираюсь делать за вас вашу домашнюю работу.
Удачи в охоте за наносекундами! :)
2. Литература — Архив WASM.RU

Множество полезной литературы и туториалов можно скачать бесплатно с www-сайта Intel или заказать на CD-ROM. Рекомендуется, чтобы вы изучили эту литературу для ознакомления с архитектурой процессора. Тем не менее, документы от Intel не всегда точны — особенно много ошибок содержат туториалы (похоже, что они не проверяют собственные примеры).
Я не будут давать здесь прямые ссылки, потому что местоположение файлов часто меняется. Вы можете найти нужные вам документы используя функцию поиска на developer.intel.com или перейдя по ссылке, которые находятся на www.agner.org/assem.
Некоторые документы в формате .PDF. Если у вас нет никакого программного обеспечения для просмотра или распечатки PDF-файлов, вы можете скачать просмотровщик файлов Acrobat с www.adobe.com.
VTUNE — это утилита от Intel для оптимизации кода. Я не тестировал его и поэтому не могу ничего сказать по этому поводу.
Кроме Intel существует множество другой полезной информации. Эти источники перечислены в FAQ ньюсгруппы comp.lang.asm.x86. Также обратите внимание на ссылки, приведенные на www.agner.org/assem.
3. Вызов ассемблерных функций из языка высокого уровня — Архив WASM.RU

Вы можете использовать встроенный ассемблер или написать процедуру полностью на ассемблере и вставить ее в свой проект. Если вы выберете последний вариант, рекомендуется, что компилятор умел переводить высокоуровневый код напрямую в ассемблер. Это даст вам гарантию, что вы получите верный метод вызова функции. Большинство компиляторов C++ умеют это делать.
Методы вызова функций и искажение имен функций (англ. mangling) могут быть достаточно сложными. Существует много различных соглашений о вызовах функций, и разные марки компиляторов не совместимы друг с другом в этом отношении. Если вы вызываете ассемблерную процедуру из C++, самым логичным и совместимым будет объявление вашей функции как extern "C" и _cdecl. К имени ассемблерной функции надо прибавить символ подчеркивания и компилировать ее нужно указав чувствительность к регистрам в именах внешних функций и переменных (опция -mx).
Если вам нужно использовать перегружаемые функции, перегружаемые операторы, функции-члены классов и другие расширения C++, тогда вам нужно сначала написать это на C++ и заставить ваш компилятор перевести это в ассемблер, чтобы получить верную информацию о линковке и методе вызова, так как они могут отличаться у разных компиляторов. Если вам нужна ассемблерная функция, соглашение о вызове которой отличается от extern "C" и _cdecl, вам нужно получить ее имя, создаваемое компилятором. Вот пример перегруженной функции вычисления квадратного корня:
Код (ASM):
  1.  ; int square (int x);
  2.   SQUARE_I PROC NEAR             ; целочисленная функция вычисления квадратного корня
  3.   @square$qi LABEL NEAR          ; имя, создаваемое компилятором Borland
  4.   ?square@@YAHH@Z LABEL NEAR     ; имя, создаваемое компилятором Microsoft
  5.   _square__Fi LABEL NEAR         ; имя, создаваемое компилятором Gnu
  6.   PUBLIC @square$qi, ?square@@YAHH@Z, _square__Fi
  7.           MOV     EAX, [ESP+4]
  8.           IMUL    EAX
  9.           RET
  10.   SQUARE_I ENDP
  11.  
  12.   ; double square (double x);
  13.   SQUARE_D PROC NEAR             ; функция вычисления квадратного корня с двойной точностью
  14.  
  15.   @square$qd LABEL NEAR          ; имя, создаваемое компилятором Borland
  16.   ?square@@YANN@Z LABEL NEAR     ; имя, создаваемое компилятором Microsoft
  17.   _square__Fd LABEL NEAR         ; имя, создаваемое компилятором Gnu
  18.   PUBLIC @square$qd, ?square@@YANN@Z, _square__Fd
  19.           FLD     QWORD PTR [ESP+4]
  20.           FMUL    ST(0), ST(0)
  21.           RET
  22.   SQUARE_D ENDP
Способ передачи параметров зависит от соглашения о вызове функции:
соглашениепорядок помещения параметров в стеккто очищает стек
_cdeclобратныйвызывающий
_stdcallобратныйпроцедура
_fastcallзависит от компиляторапроцедура
_pascalпрямойпроцедура
Использование регистров в 16-ти битном режиме или Windows, C или C++

  • 16-ти битное значение возвращается в AX
  • 32-х битное значение в DX:AX
  • значение с плавающей запятой в ST(0).
  • Регистры AX, BX, CX, DX, ES и арифметические флаги могут быть изменены процедурой;
  • другие регистры должны быть сохранены и восстановлены.
  • Процедура может полагаться на то, что при вызове другой процедуры значения
    в регистрах SI, DI, BP, DS и SS не изменится.
Использование регистров в 32-х битных Windows, C++ и других языках программирования

  • целочисленное значение возвращается в EAX
  • значение с плавающей точкой возвращается в ST(0)
  • Регистры EAX, ECX, EDX (но не EBX) могут быть изменены процедурой;
  • Другие регистры (EBX, EBP, EDI, ESI, ESP) должны быть сохранены и восстановлены.
  • Сегментные регистры нельзя изменять даже временно.
    • CS, DS, ES и SS указывают на плоский сегмент.
    • FS используется операционной системой.
    • GS не используется, но зарезервирован.
  • Флаги могут меняться процедурой, но со следующими ограничениями:
    • флаг направления равен 0 по умолчанию. Его можно изменить временно, но при выходе из процедуры необходимо очистить.
    • Флаг прерывания не может быть очищен.
  • Стековый регистр плавающей запятой пуст при входе в процедуру и должен быть пустым при выходе, если только ST(0) не используется для возвращения значения.
  • Регистры MMX можно изменять, и если это было сделано, их нужно очистить с помощью инструкции EMMS перед возвращением или вызовом любой другой процедуры, которая может использовать регистры плавающей запятой.
  • Все XMM регистры можно свободно изменять. Правила для передачи параметров и возвращения значений через регистры XMM объяснены в интеловском документе AP 589.
  • Процедура может полагаться на неизменность EBX, ESI, EDI, EBP и всех сегментных регистров при вызове другой процедуры.
4. Отладка — Архив WASM.RU

Отладка ассемблерного кода может оказаться довольно трудоемкой и неприятной, как вы, возможно, уже заметили. Я рекомендую вам
  1. сначала написать то, что вы хотите оптимизировать как подпрограмму на языке высокого уровня.
  2. Затем напишите тестовую программу, в которой вы будете отлаживать подпрограмму.
  3. Убедитесь, что тестовая программа удовлетворяет всем условиям ветвления и выравнивания.
  4. Затем переведите код на язык ассемблера.
Теперь вы можете начать оптимизировать. Каждый раз, когда вам нужно сделать изменения, вы будете переходить к тестовой программе, чтобы убедиться, что она работает. пронумеруйте все ваши версии и сохраните их, чтобы вы могли в случае ошибки вернуться к ним.
  • Протестируйте скорость наиболее критичных частей вашей программы с помощью метода, изложенного в главе 30 или с помощью тестовой программы.
  • Если код значительно медленнее, чем ожидалось, тогда возможно, что:
    • неправильно используется кэш (глава 7),
    • невыровненные операнды (глава 6),
    • цена первого запуска (глава 8),
    • неправильное предсказание переходов (глава 22),
    • проблемы загрузки кода (глава 15),
    • потери скорости при чтении регистра (глава 16)
    • или долгая цепь зависимости (глава 20).
5. Модель памяти — Архив WASM.RU

Пентиумы спроектированы в основном для 32-х разрядного кода, и скорость исполнения ужасно медлена при использовании 16-х разрядного кода. Сегментирование вашего кода и данных также значительно отражается на производительности, поэтому вам следует использовать 32-х разрядный плоский режим и операционную систему, которая поддерживает этот режим. Примеры, приводимые в данном руководстве — для 32-х разрядный плоского режима, если иное не оговорено особо.
6. Выравнивание — Архив WASM.RU

Все данные в RAM должны быть выровненены так, чтобы их адреса были кратны 2, 4, 8 или 16 согласно таблице:
размервыравнивание
операнда
PPlain и PMMX
PPro, PII и PIII
1 (byte)11
2 (word)22
4 (dword)44
6 (fword)48
8 (qword)88
10 (tbyte)816
16 (oword) n.a. 16
На PPlain и PMMX при обращении к невыровненным данным будет теряться по меньшей мере 3 такта, если пересечена граница в 4 байта. Потери будут выше при пересечении границы кэша.
На PPro, PII и PIII невыровненные данные будут стоить вам 6-12 дополнительных тактов, если пересечена граница кэша. Невыровненные операнды, меньшие чем 16 байтов и не перешедшие границу в 32 байта, не приводят к потерям.
Выравнивание данных на 8 или 16 в стеке двойных слов может стать проблемой. Общий метод решения — установить выравненный указатель на кадр стека. Функция с выравненными локальными данными может выглядеть примерно так:
Код (ASM):
  1. _FuncWithAlign PROC NEAR
  2.         PUSH    EBP                        ; пролог
  3.         MOV     EBP, ESP
  4.         AND     EBP, -8                    ; выравнивание указателя на кадр
  5.                                            ;   стека на 8
  6.         FLD     DWORD PTR [ESP+8]          ; параметр функции
  7.         SUB     ESP, LocalSpace + 4        ; резервируем локальные данные
  8.         FSTP    QWORD PTR [EBP-LocalSpace] ; теперь сохраняем что-нибудь в
  9.                                            ; локальной переменной
  10.         ...
  11.         ADD     ESP, LocalSpace + 4        ; эпилог, восстанавливаем ESP
  12.  
  13.         POP     EBP                        ; (потеря скорости AGI на PPlain/PMMX)
  14.         RET
  15. _FuncWithAlign ENDP
В то время как выравнивание данных важно всегда, выравнивание кода не является необходимым на PPlain и PMMX. Принципы выравнивания кода на PPro, PII и PIII изложены в главе 15.
7. Кэш — Архив WASM.RU

У PPlain и PPro 8 килобайт кэша первого уровня для кода и 8 килобайт для данных. У PMMX, PII и PIII по 16 килобайт для кода и данных. Данные в кэше первого уровня можно читать или перезаписывать всего лишь за один такт, в то время как выход за границы кэша может стоить множества тактов. Поэтому важно, понимать, как работает кэш, чтобы использовать его более эффективно.
Кэш данных состоит из 256 или 512 рядов по 32 байта в каждом. Каждый раз, когда вы считываете элемент данных, который еще не находится в кэше, процессор считает весь кэш-ряд из памяти. Кэш-ряды всегда выравниваются по физическому адресу, кратному 32. Когда вы считываете байт по адресу, который кратен 32, чтение или запись в следующие 31 байт вам не будет стоить практически ничего. Вы можете использовать это к своей выгоде, организовав данные, которые используются вместе, в блоки по 32 байта. Если, например, у вас есть цикл, в котором обрабатываются два массива, вы можете перегруппировать эти два массива в один массив структур, чтобы данные, которые используются вместе, находились в памяти рядом друг с другом.
Если размер массива кратен 32 байтам, вам следует и выравнивать его по границе 32 байта.
Кэш set-ассоциативен. Это означает, что кэш-ряд нельзя ассоциировать с произвольным адресом памяти. У каждого кэш-ряда есть 7-ми битное значение, которое задает биты 5-11 адреса в физической памяти (биты 0-4 определяются положением элемента данных в кэш-ряде). У PPlain и PPro два кэш-ряда для каждого из возможных 128 set-значений, поэтому с определенным адресом в памяти можно ассоциировать только два жестко заданных кэш-ряда. В PMMX, PII и PIII — четыре.
Следствием этого является то, что кэш может содержать не более двух или четырех различных блока данных, у которых совпадают биты 5-11 их адресов. Вы можете определить, совпадают ли эти биты у двух адресов следующим образом: удалите нижние пять битов каждого адреса, чтобы получить значение, кратное 32. Если разница между этими обрезанными значениями кратна 4096 (=1000h), значит у этих адресов одно и то же set-значение.
Давайте я проиллюстрирую вышеизложенное с помощью следующего кода, где ESI содержит адрес, кратный 32:
Код (ASM):
  1. AGAIN:  MOV  EAX, [ESI]
  2.         MOV  EBX, [ESI + 13*4096 +  4]
  3.         MOV  ECX, [ESI + 20*4096 + 28]
  4.         DEC  EDX
  5.         JNZ  AGAIN
У всех трех адресов, использованных здесь, совпадают set-значения, потому что разница между обрезанными адресами будет кратна 4096. Этот цикл будет очень плохо выполняться на PPlain и PPro. Во время записи в ECX нет ни одного свободного кэш-ряда, поэтому тот, который был использован для значения, помещенного в EAX, заполняется данными с [ESI+20*4096] до [ESI+20*4096+31] и записывает значение в ECX. Затем во время чтения EAX оказывается, что кэш-ряд, содержавший значение для EAX, уже изменен, поэтому то же самое происходит с кэш-рядом, содержащим значение для EBX, и так далее. Это пример нерационального использования кэша: цикл занимает около 60 тактов. Если третью линию изменить на:
Код (ASM):
  1.         MOV  ECX, [ESI + 20*4096 + 32]
мы пересечем границу в 32 байта, поэтому у нас будет другое set-значение, и не возникнет никаких проблем с ассоциирование кэш-рядов к этим трем адресам. Цикл теперь занимает 3 такта (не считая первого раза) — весьма значительное улучшение! Стоит упомянуть, что у PMMX, PII и PIII для каждого set-значения есть четыре кэш-ряда. (Некоторые интеловские документы ошибочно утверждают, что у PII по два кэш-ряда на каждое set-значение).
Определить, одинаковые или у переменных set-значения, может оказаться достаточно сложным, особенно, если они разбросаны по разным сегментам. Лучшим, что вы можете придумать для избежания проблем подобного рода — это держать все данные, используемые в критической части вашей программы внутри одного блока, который будет не больше, чем кэш, либо в двух блоках, не больших, чем половина от его размера (например, один блок для статических данных, а другой для данных в стеке). Это будет гарантией того, что ваши кэш-ряды используются оптимальным образом.
Если критическая часть вашего кода имеет дело с большими структурами данных или данными, располагающимися в памяти случайным образом, тогда вам стоит подумать о том, чтобы держать наиболее часто используемые переменные (счетчики, указатели, флаговые переменные и так далее) в одном блоке с максимальным размеров в 4 килобайта, чтобы использовались все кэш-ряды. Так как вам еще требуется стек для параметров подпрограммы и возвращаемых адресов, лучшее, что вы можете сделать — это скопировать наиболее часто используемые статические данные в динамические переменные в стеке, а после выхода из критической части кода скопировать обратно те из них, которые подверглись изменению.
Чтение элемента данных, которого нет в кэше первого уровня приводит к тому, что весь кэш-ряд будет заполнен из кэша второго уровня. Если ее нет и в кэше второго уровня, то вы получите еще большую задержку, которая может оказаться совсем гигантской, если вы пересечете границу между страницами памяти.
При считывании больших блоков данных из памяти, скорость ограничена временем, уходящим на заполнение кэш-рядов. Иногда вы можете увеличить скорость, считывая данные в непоследовательном порядке: еще не считав данные из одного кэш-ряда, начать читать первый элемент из следующего кэш-ряда. Этот метода можете повысить скорость на 20-40% при считывании данных из основной памяти или кэша второго уровня на PPlain и PMMX, или из кэша второго уровня на PPro, PII и PIII. Недостаток этого метода заключается в том, что код программы становится очень запутанным и сложным. Другую информацию об этом методе вы можете получить на www.intelligentfirm.com.
Когда вы пишите в адрес, которого нет в кэше первого уровня, тогда значение пойдет прямо в кэш второго уровня или в память (в зависимости от того, как настроен кэш второго уровня) на PPlain и PMMX. Это займет примерно 100 ns. Если вы пишите восемь или более раз в тот же 32-х байтный блок памяти, ничего не читая из него, и блок не находится в кэше первого уровня, возможно стоит сделать фальшивое чтение из него, чтобы он был загружен в кэш-ряд. Все последующие чтения будут производиться в кэш, что занимает всего лишь один такт. На PPlain и PMMX иногда происходит небольшая потеря в производительности при повторяющемся чтении по одному и тому же адресу без чтения из него между этим.
На PPro, PII и PIII запись обычно ведет к загрузке памяти в кэш-ряд, но возможно указать область памяти, с которой следует поступать иначе (смотри Pentium Pro Family Developer's Manual, vol. 3: Operating System Writer's Guide").
Другие пути увеличения скорости чтения и записи в память обсуждаются в главе 27.8.
У PPlain и PPro есть два буфера записи, у PMMX, PII и PIII — четыре. На PMMX, PII и PIII у вас может быть до четырех незаконченных записей в некэшированную память без задержки последующих инструкций. Каждый буфер записи может обрабатывать операнды до 64 бит длиной.
Временные данные удобно хранить в стеке, потому что стековая область как правило находится в кэше. Тем не менее, вам следует избегать проблем с выравниваем, если элементы ваших данных больше, чем размер слова стека.
Если время жизни двух структур данных не пересекается, они могут разделять одну и ту же область памяти, чтобы повысить эффективность кэша. Это согласуется с общей практикой держать временные переменные в стеке.
Хранение временных данных в регистрах, разумеется, более эффективно. Так как регистров мало, вы, возможно, захотите использовать [ESP] вместо [EBP] для адресации данных к стеку, чтобы освободить EBP для других целей. Только не забудьте, что значение ESP меняется каждый раз, когда вы делаете PUSH или POP. (Вы не можете использовать ESP под 16-ти битным Windows, потому что прерывание таймера будет менять верхнее слово ESP тогда, когда это невозможно предугадать.)
Есть отдельных кэш для кода, которых схож с кэшем данных. Размер кэша кода равен 8 килобайтам на PPlain и PPro и 16 килобайтам на PMMX, PII и PIII. Важно, чтобы критические части вашего кода (внутренние циклы) умещались в кэш кода. Часто используемые процедуры следует помещать рядом друг с другом. Редко используемые ветви или процедуры нужно держать в самом низу вашего кода или где-нибудь еще.
8. Исполнение кода в первый раз — Архив WASM.RU

Обычно исполнение кода в первый раз занимает намного больше, чем при последующих повторениях в силу следующих причин:
  1. Загрузка кода из RAM в кэш занимает намного больше времени, чем его выполнение.
  2. Любые данные, к которым обращается код должны быть загружены в кэш, что также занимает много времени. Когда код повторяется, данные, как правило, уже находятся в кэше.
  3. Инструкция перехода не будет находится в буфере предсказывания переходов в первый раз, поэтому маловероятно, что она будет предсказана правильно. Смотри главу 22.
  4. На PPlain декодирование инструкций является узким местом. Если определение длины инструкции занимает один такт, то процессор не может раскодировать за такт две инструкции, так как он не знает, где начинает вторая инструкция. PPlain решает эту проблему, запоминая длину любой инструкции, которая осталась в кэше. Как следствие, ряд инструкции не спариваются на PPlain во время первого исполнения, если только первые две инструкции не занимают по одному байту. У PMMX, PPro, PII и PIII таких потерь нет.
В силу этих четырех причин, код внутри цикла будет занимать больше времени, исполняясь в первый раз, нежели в последующие.
Если у вас большой цикл, который не влезает в кэш кода, у вас будут постоянные потери в производительности, потому что код будет исполняться не из кэша. Вам следует реорганизовать цикл, чтобы он умещался в кэш.
Если в вашем цикле очень много переходов и вызовов, то у вас будут потери из-за регулярных ошибок предсказания переходов.
Также у вас будет потеря производительности, если ваш цикл периодически обращается к структурам данных, которые слишком велики для кэша данных.
9. Задержка генерации адреса — Архив WASM.RU

Чтобы высчитать адрес в памяти, который нужен инструкции, требуется один такт. Обычно эти вычисления делаются одновременно с выполнение предыдущей инструкции или спаренных инструкций. Но если адрес зависит от результат инструкции, которая выполнялась в предыдущем такте, тогда вам придется подождать дополнительный такт, чтобы получить требуемый адрес. Это называется задержкой AGI.
Код (ASM):
  1.     ADD EBX,4 / MOV EAX,[EBX] ; задержка AGI
Задержку в данном примере можно убрать, если поместить какие-нибудь другие инструкции между 'ADD EBX, 4' и 'MOV EAX, [EBX]' или переписав код следующим образом:
Код (ASM):
  1.     MOV EAX,[EBX+4]
  2.     ADD EBX,4
Также вы можете получить задержку AGI при использовании инструкции, которые используют ESP для адресации, например, PUSH, POP, CALL и RET, если ESP был изменен в предыдущем такте такими инструкциями как MOV, ADD и SUB. У PPlain и PMMX есть специальная схема, чтобы предсказывать значение ESP после стековой операции, поэтому у вас не будет задержки AGI при изменении ESP с помощью PUSH, POP или CALL. При использовании RET она может случиться только если у него есть числовой операнд, который прибавляется к ESP.
Код (ASM):
  1.     ADD ESP,4 / POP ESI            ; задержка AGI
  2.     POP EAX   / POP ESI            ; нет задержки, спариваются
  3.     MOV ESP,EBP / RET              ; задержка AGI
  4.     CALL L1 / L1: MOV EAX,[ESP+8]  ; нет задержки
  5.     RET / POP EAX                  ; нет задержки
  6.     RET 8 / POP EAX                ; задержка AGI
Инструкция LEA также может стать причиной задержки AGI, если она использует базовый или индексный регистр, который был изменен в предыдущем такте.
Код (ASM):
  1.     INC ESI
  2.     LEA EAX,[EBX+4*ESI]  ; задержка AGI
У PPro, PII и PIII нет задержки AGI для чтения из памяти и LEA, но есть задержка при записи в память. Это не очень важно, если только последующий код не должен ждать, пока операция записи не будет выполнена.
10. Спаривание целочисленных инструкций (PPlain и PMMX) — Архив WASM.RU

10.1 Совершенное спаривание
У PPlain и PMMX есть два конвейера, выполняющих инструкции, которые называются U-конвейер и V-конвейер. В определенных условий можно выполнить две инструкции одновременно, одну в U-конвейере, а другую в V-конвейере. Это практически удваивает скорость. Поэтому имеет смысл перегруппировать ваши инструкции и сделать их спариваемыми.
  1. Следующие инструкции могут находится в любом конвейере.
    • MOV регистр, память или уже заданного числа в регистр или память
    • PUSH регистр или число, POP регистр
    • LEA, NOP
    • INC, DEC, ADD, SUB, CMP, AND, OR, XOR,
    • и некоторые разновидности TEST (смотри главу 26.14)
  2. Следующие инструкции спариваются только в U-конвейере:
    • ADC, SBB
    • SHR, SAR, SHL, SAL с число в качестве аргумента
    • ROR, ROL, RCR, RCL с единицей в качестве аргумента
  3. Следующие инструкции спариваются только в V-конвейере:
    • ближний вызов
    • короткий и ближний переход
    • короткий и ближний условный переход
  4. Все другие целочисленные инструкции могут выполняться только в U-конвейере и не могут спариваться.
  5. Две следующие друг за другом инструкции будут спариваться, если соблюдены следующие условия:
    • Первая инструкция спаривается в U-конвейере, а вторая — в V-конвейере.
    • Вторая инструкция не читает и не пишет из регистра, если пишет первая инструкция.
    Код (ASM):
    1.     MOV EAX, EBX / MOV ECX, EAX     ; чтение после записи, не спаривается
    2.     MOV EAX, 1   / MOV EAX, 2       ; запись после записи, не спаривается
    3.     MOV EBX, EAX / MOV EAX, 2       ; запись после чтения, спаривается
    4.     MOV EBX, EAX / MOV ECX, EAX     ; чтение после чтения, спаривается
    5.     MOV EBX, EAX / INC EAX          ; чтение и запись после чтения, спаривается
  6. Неполные регистры считаются полными регистрами.
    Код (ASM):
    1.     MOV AL, BL  /  MOV AH, 0
    пишут в разные части одного и того же регистра, не спаривается.
  7. Две инструкции, пишущие в разные части регистра флагов могут спариваться не смотря на правила 2 и 3.
    Код (ASM):
    1.     SHR EAX, 4 / INC EBX            ; спаривается
  8. Инструкция, которая пишет в регистр флагов может спариваться с условным переходом несмотря на правило 2.
    Код (ASM):
    1.     CMP EAX, 2 / JA LabelBigger     ; спаривается
  9. Следующая комбинация инструкций может спариваться несмотря на тот факт, что обе изменяют указатель на стек:
    PUSH + PUSH, PUSH + CALL, POP + POP
  10. Есть ограничения на спаривание инструкций с префиксами. Есть несколько типов префиксов:
    • у инструкций, обращающихся к сегменту, не являющемся сегментом по умолчанию, есть сегментный префикс.
    • у инструкций, использующих 16-ти битные данные в 32-ом режиме или 32-х битные данные в 16-битном режиме, есть префикс размера операнда.
    • у инструкций, использующих 32-х битную адресацию через регистры в 16-ти битном режиме, есть префикс размера адреса.
    • у повторяющихся есть префикс повторения.
    • у закрытых инструкций есть префикс LOCK.
    • у многих инструкций, которых не было на процессоре 8086 есть двухбайтный опкод, чей первый байт равен 0Fh. Байт 0FH считается префиксом только на PPlain. Наиболее часто используемые инструкции с этим префиксом следующие: MOVZX, MOVSX, PUSH FS, POP FS, PUSH GS, POP GS, LFS, LGS, LSS, SETcc, BT, BTC, BTR, BTS, BSF, BSR, SHLD, SHRD, и IMUL с двумя операндами и без числового операнда.
    На PPlain инструкция с префиксом может выполняться только в U-конвейере, кроме ближних условных переходов.
    На PMMX инструкции с префиксами размера операнда, размера адреса или 0FH могут выполняться в любом конвейере, в то время как инструкции с префиксами сегмента, повторения или LOCK могут выполняться только в U-конвейере.
  11. Инструкция, в которой используется одновременно адресация со смещение и числовые данные на PPlain не спариваются, на PMMX могут спариваться только в U-конвейере:
    Код (ASM):
    1.     MOV DWORD PTR DS:[1000], 0    ; не спаривается или только в U-конвейере
    2.     CMP BYTE PTR [EBX+8], 1       ; не спаривается или только в U-конвейере
    3.     CMP BYTE PTR [EBX], 1         ; спаривается
    4.     CMP BYTE PTR [EBX+8], AL      ; спаривается
    (Другая проблема, связанная с подобными инструкциями, выполняющимися на PMMX, заключается в том, что такие инструкции могут быть длиннее семи байтов, а это приводит к невозможности раскодировки больше одной инструкции за такт, подробнее это объясняется в главе 12.)
  12. Обе инструкции должны быть заранее загружены и раскодированы. Это объяснение в главе 8.
  13. Есть специальные правила спаривания для инструкций MMX на PMMX:
    • MMX-сдвиг, инструкции упаковки или распаковки могут выполнять в любом конвейере, но не могут спариваться с другим MMX-сдвигом, инструкциями упаковки или распаковки.
    • MMX-инструкции умножения могут выполняться в любом конвейер, но не могут спариваться с другими MMX-инструкциями умножения. Они занимают 3 такта, и последние 2 такта могут перекрыть последующие инструкции, также как это делают инструкции плавающей запятой (смотри главу 24).
    • MMX-инструкция, обращающаяся к памяти или целочисленным регистрам может выполняться только в U-конвейер и не могут спариваться с не-MMX инструкцией.

0 2.633
Mikl___

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

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