Оптимизация для процессоров семейства Pentium: 24. Работа с плавающей запятой (PPlain и PMMX) — Архив WASM.RU
Инструкции плавающей запятой не могут спариваться так, как это делают целочисленные инструкции, не считая некоторых случаев, определяемых следующими правилами:
- первая инструкция (выполняющаяся в U-конвеере) должна быть FLD, FADD, FSUB, FMUL, FDIV, FCOM, FCHS или FABS.
- вторая инструкция (в V-конвеере) должна быть FXCH.
- инструкция, следующая за FXCH, должна быть инструкцией плавающей запятой, иначе FXCH спарится несовершенно и займет лишний такт.
Это особенное спаривание играет важную роль, что вкратце будет объяснено.
Хотя, как правило, инструкции плавающей запятой не могут спариваться, многие из них конверизуются, то есть одна инструкция может начать выполнение до того, как будет закончена предыдущая инструкция.
Пример:
Код (Text):
FADD ST(1),ST(0) ; такты 1-3 FADD ST(2),ST(0) ; такты 2-4 FADD ST(3),ST(0) ; такты 3-5 FADD ST(4),ST(0) ; такты 4-6Очевидно, что выполнение двух инструкциий не может пересекаться, если второй инструкции нужен результат первой. Так как почти все инструкции плавающей запятой работают с вершиной стека регистров ST(0), возможностей сделать их независимыми друг от друга не очень много. Решение этой проблемы состоит в переименовании регистров. Инструкция FXCH в реальности не обменивает содержимое двух регистров, оно только меняет их имена. Инструкции, которые помещают или извлекают значение из стека регистров также работают с помощью переименования. Переименование регистров на Pentium настолько хорошо оптимизировано, что можно переименовать использующийся регистр. Переименование регистров никогда не вызывает задержек - возможно даже переименовать регистр более чем один раз за такт, например, когда вы спариваете FLD или FCOMPP с FXCH.
Правильно используя инструкции FXCH, вы можете создать условия, чтобы инструкции плавающего кода были более независимыми друг от друга.
Пример:
Код (Text):
FLD [a1] ; такт 1 FADD [a2] ; такт 2-4 FLD [b1] ; такт 3 FADD [b2] ; такт 4-6 FLD [c1] ; такт 5 FADD [c2] ; такт 6-8 FXCH ST(2) ; такт 6 FADD [a3] ; такт 7-9 FXCH ST(1) ; такт 7 FADD [b3] ; такт 8-10 FXCH ST(2) ; такт 8 FADD [c3] ; такт 9-11 FXCH ST(1) ; такт 9 FADD [a4] ; такт 10-12 FXCH ST(2) ; такт 10 FADD [b4] ; такт 11-13 FXCH ST(1) ; такт 11 FADD [c4] ; такт 12-14 FXCH ST(2) ; такт 12В вышеприведенном примере мы создали три независимые ветви. Каждый FADD занимает 3 такта, поэтому мы можем каждый такт начинать выполнение нового FADD. Когда мы начали выполнение ветви 'a', у нас есть время, чтобы начать выполнение двух новых инструкций FADD в ветвях 'b' и 'c' до возвращения к ветви 'a', поэтому каждый третий FADD принадлежит той же ветви. Мы используем инструкции FXCH каждый раз, когда необходимо, чтобы ST(0) стал равен регистру, который принадлежит к желаемой ветви. Как вы можете видеть из примера, это образует регулярную последовательность, но уясните хорошо, что инструкции FXCH повторяются с периодом, равным двум, в то время как у ветвей период равен трем. Это может немного смущать, поэтому вам следует проработать этот пример, чтобы понять где находится какой из регистров.
Все версии инструкций FADD, FSUB, FMUL и FILD занимают три такта и конвееризуются, поэтому вышеописанный метод можно применять и с этими инструкциями. Использование переменных в памяти не отнимает больше времени, чем использование регистров, если переменная в памяти находится в кэше первого уровня и правильно выравнена.
Вы уже, наверное, что у всех правил есть исключения, и у вышеизложенного они тоже есть: вы не можете начать выполнение инструкции FMUL на следующий такт после другой инструкции FMUL, потому что FMUL не может спариваться совершенно. Рекомендуется, чтобы вы поместили другую инструкцию между двумя FMUL'ами.
Пример:
Код (Text):
FLD [a1] ; такт 1 FLD [b1] ; такт 2 FLD [c1] ; такт 3 FXCH ST(2) ; такт 3 FMUL [a2] ; такты 4-6 FXCH ; такт 4 FMUL [b2] ; такты 5-7 (задержка) FXCH ST(2) ; такт 5 FMUL [c2] ; такты 7-9 (задержка) FXCH ; такт 7 FSTP [a3] ; такты 8-9 FXCH ; такт 10 (неспарены) FSTP [b3] ; такты 11-12 FSTP [c3] ; такты 13-14Здесь у вас есть задержка между FMUL [b2] и между FMUL [c2], потому что другая FMUL началась в предыдущий такт. Вы можете улучшить этот код, поместив инструкции FLD между FMUL'ами.
Код (Text):
FLD [a1] ; такт 1 FMUL [a2] ; такт 2-4 FLD [b1] ; такт 3 FMUL [b2] ; такт 4-6 FLD [c1] ; такт 5 FMUL [c2] ; такт 6-8 FXCH ST(2) ; такт 6 FSTP [a3] ; такт 7-8 FSTP [b3] ; такт 9-10 FSTP [c3] ; такт 11-12В других случаях вы можете поместить FADD, FSUB или что-нибудь еще между FMUL'ами, чтобы избежать задержек.
Инструкции плавающей запятой, чье выполнение пересекается, требуют, конечно, чтобы у вас были независимые ветви, выполненине которых вы можете совместить. Если у вас есть только одна большая формула, которую надо выполнить, тогда вы можете посчитать части формулы параллельно, чтобы достигнуть цели. Если, например, вы хотите добавить шесть чисел, тогда вы можете разделить операции на две ветви с тремя числами в каждой, и добавить две ветви в конце:
Код (Text):
FLD [a] ; такт 1 FADD [b] ; такты 2-4 FLD [c] ; такт 3 FADD [d] ; такты 4-6 FXCH ; такт 4 FADD [e] ; такты 5-7 FXCH ; такт 5 FADD [f] ; такты 7-9 (задержка) FADD ; такты 10-12 (задержка)Здесь у нас есть задержка в один такт перед FADD [f], потому что она ожидает результата выполнения FADD [d] и задержка в два такта перед последним FADD, потому что она ожидает результата FADD [f]. Более поздняя задержка может быть спрятана путем заполнения ее несколько целочисленными инструкциями, но с первой задержкой это не получится, так как целочисленная инструкция в этом месте приведет к тому, что FXCH будет спариваться несовершенно.
Первую задержку можно избежать, создав три ветви вместо двух, но это будет стоить дополнительно FLD, поэтому мы ничего не выиграем, прибегнув к этому варианту, если только у нас нет восьми чисел, которые нужно сложить.
Выполнение не все инструкций плавающей запятой может пересекаться. И выполнение некоторых инструкций плавающей запятой лучше сочетается с целочисленными инструкциями. Например, инструкция FDIV занимает 39 тактов. Во все, кроме первого такта, выполнение этой инструкции может пересекаться с целочисленными инструкциями, но только в последние два такта она может пересекаться с инструкциями плавающей запятой.
Пример:
Код (Text):
FDIV ; такт 1-39 (U-конвеер) FXCH ; такт 1-2 (V-конвеер, несовершенное спаривание) SHR EAX,1 ; такт 3 (U-конвеер) INC EBX ; такт 3 (V-конвеер) CMC ; такт 4-5 (не спаривается) FADD [x] ; такт 38-40 (U-конвеер, ждет, пока FPU не освободится) FXCH ; такт 38 (V-конвеер) FMUL [y] ; такт 40-42 (U-конвеер, ждет результат FDIV)Первый FXCH спаривается с FDIV, но занимает дополнительный такт, потому что за ней не следует инструкция плавающей запятой. Пара SHR / INC начинает свое выполнение до того, как будет закончено выполнение FDIV, но была вынуждена подождать, пока свое выполнение закончит FXCH.
Если у вас нет ничего, что поместить после инструкции плавающей запятой, которая может выполняться одновременно с целочисленной инструкцией, (FDIV, FSQRT), вы можете поместить чтение значение из какой-нибудь переменной в памяти, которое может вам понадобиться в дальнейшем, чтобы она на 100% была в кэше.
Пример:
Код (Text):
FDIV QWORD PTR [EBX] CMP [ESI],ESI FMUL QWORD PTR [ESI]Здесь мы загружаем загружаем значение в [ESI] в кэш, в то время как FDIV вычисляется (результат самой операции сравнения нам не важен).
В главе 28 приведен полный список инструкций плавающей запятой, и с чем они могут спариваться.
Никаких потерь при использовании переменных в памяти в инструкциях плавающей запятой, потому что модуль арифметических вычислений на один шаг дальше в конвеере, чем модуль чтения. Однако при сохранении данных может случиться задержка, аналогичная задержке AGI: выполнение инструкции FST или FSTP с переменной в памяти в качестве операнда занимает два такта, но данные должны быть готовы в предыдущем такте, поэтому будет задержка в один такт, если значение, которое нужно сохранить не будет готово еще в предыдущем такте.
Пример:
Код (Text):
FLD [a1] ; такт 1 FADD [a2] ; такт 2-4 FLD [b1] ; такт 3 FADD [b2] ; такт 4-6 FXCH ; такт 4 FSTP [a3] ; такт 6-7 FSTP [b3] ; такт 8-9FSTP задерживается на один такт, потому что результат FADD не был готов в предыдущем такте. Во многих случаях вы не можете скрыть этот тип задержек бзе организования вашего кода с плавающей запятой в четыре ветви или помещения внутрь каких-то целочисленных инструкций. Два такта на стадии выполнения инструкции FST(P) не могут спариваться или пересекаться с любой другой последующей инструкцией.
Инструкции с целочисленными операндами, такими как FIADD, FISUB, FIMUL, FIDIV, FICOM можно разделить на простые операции, чтобы улучшить пересекаемость выполнений инструкций.
Пример:
Код (Text):
FILD [a] ; clock cycle 1-3 FIMUL [b] ; clock cycle 4-9Разделить на:
Код (Text):
FILD [a] ; такты 1-3 FILD [b] ; такты 2-4 FMUL ; такты 5-7В этом примере вы экономите два такта, пересекая выполнение двух инструкций FILD. © Агнер Фог, пер. Aquila
Оптимизация для процессоров семейства Pentium: 24. Работа с плавающей запятой (PPlain и PMMX)
Дата публикации 22 авг 2002