Оптимизация для процессоров семейства Pentium: 26. Проблемные инструкции — Архив WASM.RU
26.1 XCHG (все процессоры)
Инструкция 'XCHG регистр, [память]' опасна. По умолчанию эта инструкция имеет неявный префикс LOCK, что не дает ей загружаться в кэш. Поэтому выполнение данной инструкции отнимает очень много времени, и ее следует избегать.
26.2 Вращение через флаг переноса (все процессоры)
RCR и RCL, сдвигающие более, чем один бит, медленны, и их следует избегать.
26.3 Строковые инструкции (все процессоры)
Строковые инструкции без префикса повторения слишком медленны, и их следует заменить более простыми инструкциями. То же самое относится к LOOP на всех процессорах и к JECXZ на PPlain и PMMX.
REP MOVSD и REP STOSD довольно быстры, если число повторений не слишком мало. Всегда используйте версию DWORD, где это возможно, и убедитесь, что источник и приемник выравнены на 8.
Некоторые другие методы перемещения данных быстрее в определенных условиях. Подробнее смотрите главу 27.8.
Обратите внимание, что пока инструкция REP MOVS записывает слово в приемник, она считывает следующее слово из источника в том же такте. У вас может конфликт банков кэша, если биты 2-4 у этих двух адресов одни и те же. Другими словами, у вас будут неизбежные потери в один такт на итерацию, если ESI+(размер слова)-EDI кратно 32. Самый простой путь избежать конфликтов банков кэша - это использовать версию DWORD и выравнивать источник и приемник на 8. Никогда не используйте MOVSB или MOVSW в оптимизированном коде, даже в 16-ти битном.
REP MOVS и REP STOS могут выполняться очень быстро, если перемещать целую линию кэша за раз на PPro, PII и PIII:
- источник и приемник должны быть выравнены на 8
- должно быть задано направление вперед (очищен флаг направления)
- счетчик (ECX) должен иметь значение равное или большее 64
- разница между EDI и ESI должна быть численно больше или равна 32
При этих условиях количество мопов будет примерно равно 215+2*ECX для REP MOVSD и 185+1.5*ECX для REP STOSD, что дает примерную скорость в 5 байтов в такт для обоих инструкций, что в три раза больше, если какое-нибудь из вышеприведенных условий не будет соблюдено.
Версии этой инструкции для байтов и слов также выигравают от соблюдений данных условий, но они менее эффективны, чем версии для двойных слов.
REP STOSD оптимальна при тех же условиях, что и REP MOVSD
REP LOADS, REP SCAS и REP CMPS не оптимальны, и их можно заменить на циклы. Смотри пример 1.10, 2.8 и 2.9 для поиска альтернатив REPNE SCASB. REP CMPS может вызвать конфликт баноков кэша, если биты 2-4 одинаковы в ESI и EDI.
26.4 Тестирование битов (все процессоры)
Инструкции BT, BTC, BTR и BTS следует заменять инструкциями типа TEST, AND, OR, XOR или сдвигами на PPlain и PMMX. На PPro, PII и PIII битовых тестов операнда в памяти следует избегать.
26.5 Целочисленное умножение (все процессоры)
Целочисленное умножение занимает до 9 тактов на PPlain и PMMX и до 4 тактов на PPro, PII и PIII. Поэтому часто выгоднее бывает заменить умножение на константу и комбинацию других инструкций, таких как SHL, ADD, SUB и LEA.
Пример:
Код (Text):
IMUL EAX,10можно заменить на
Код (Text):
MOV EBX,EAX / ADD EAX,EAX / SHL EBX,3 / ADD EAX,EBXили
Код (Text):
LEA EAX,[EAX+4*EAX] / ADD EAX,EAXУмножение чисел с плавающей запятой быстрее, чем целочисленное умножение на PPlain и PMMX, но время, затрачиваемое на преобразование целых чисел в числа с плавающей запятой и обратную конвертацию полученного результата, обычно больше, чем время, сэкономленное в результате использования умножения с плавающей запятой, не считая тех случаев, когда количество конвертаций несравнимо с количеством умножений. Умножение MMX достаточно быстро, но доступно только для 16-ти битных операндов.
26.6 Инструкция WAIT (все процессоры)
Зачастую вы сможете повысить скорость, пренебрегнув инструкцией WAIT. Эта инструкция имеет три функции:
a. Старый процессор 8087 требовали инструкцию WAIT перед каждой инструкцией с плавающей запятой, чтобы убедиться, что сопроцессор готов ее получить.
b. WAIT используется для координирования доступа памяти между модулем вычислений плавающей запятой и модулем целочисленных вычислений.
Примеры:
Код (Text):
b.1. FISTP [mem32] WAIT ; ждем, пока FPU запишет в память, а потом.. MOV EAX, [mem32] ; считываем результат модулем целочисленных вычислений b.2. FILD [mem32] WAIT ; ждем, пока FPU считает значение из памяти.. MOV [mem32],EAX ; перед ее перезаписью целым числом b.3. FLD QWORD PTR [ESP] WAIT ; предотвращаем случайную ошибку от.. ADD ESP,8 ; перезаписи значения в стекеc. WAIT иногда используется, чтобы следить за исключениями. Он сгенерирует прерывание, если бит исключения в слове статуса FPU был установлен предыдущей операцией плавающей запятой.
Относительно a:
Эта функция WAIT была актуальна только на старом 8087. Если только вы не хотите, чтобы ваш код был совместим с 8087, вам следует указать вашему ассемблеру, чтобы он не помещал эти WAIT'ы, задав более современный процессор. Эмулятор плавающей запятой 8087 также вставляет инструкции WAIT, поэтому вам следует указать вашему ассемблеру не генерировать код эмуляции, если только он вам действительно не нужен.
Относительно b:
Инструкции WAIT для координации доступа к памяти были действительно нужны на 8087 и 80287, но на Pentium'ах она в этом качестве совершенно не обязательна. Что касается 80386 и 80486, тут ситуация не совсем ясна. Я сделал несколько тестов на этих интеловских процессорах и не смог спровоцировать ни одной ошибки, пропустив WAIT, на любом из 32-х битных интеловских процессоров, хотя руководства от Intel говорят, что WAIT необходима для этой цели, не считая инструкций FNSTSW и FNSTCW. Пропус инструкций WAIT для координирования доступа к памяти не 100% надежно даже при написании 32-х битного кода, потому что код может быть выполнен на очень редкой комбинации 80386 процессора с 287 сопроцессором, который требует WAIT. Также у меня нет информации о неинтеловских процессорах, и я не тестировал все возможные комбинации железа и программного обеспечения, поэтому могут быть ситуации, когда WAIT окажется нужен.
Если вы хотите быть уверены, что ваш коду будет работать на любом 32-х битном процессоре (включая неинтеловские процессоры), я рекомендую вам использовать WAIT в этом качестве на всякий случай.
Относительно c:
Ассемблер автоматически вставляет WAIT для этих целей перед следующими инструкциями: FCLEX, FINIT, FSAVE, FSTCW, FSTENV, FSTSW. Вы можете пропустить WAIT, написав FNCLEX и т.п. Мои тесты показывают, что в большинстве случаев WAIT не нужен, потому что эти инструкции без WAIT все равно будут генерировать прерывания или исключения, кроме FNCLEX и FNINIT на 80387. (Есть некоторая неопределенность, касаемая того, указывает ли IRET от прерывания на инструкцию FN.. или на следующую инструкцию).
Почти все инструкции плавающей запятой будут также генерировать прерывание, если предыдущая инструкция плавающей запятой установила бит исключений, поэтому исключение рано или поздно будет обнаружено. Вы можете вставить WAIT после последней инструкции плавающей запятой в вашей программе, чтобы точно поймать все исключения.
Вам все еще может понадобиться WAIT, если нужно точно знать, где случается исключение, чтобы проконтролировать ситуацию. Возьмем, например, код из b.3: если вы хотите проконтролировать исключение, которое сгенерирует в подобной ситуации FLD, вам нужен WAIT, потому что прерывание после 'ADD ESP,8' перезапишет значение, которое надо загрузить. FNOP будет быстрее, чем WAIT, и предназначается для той же цели.
26.7 FCOM + FSTSW AX (все процессоры)
Инструкция FNSTSW очень медленна на любых процессорах. У процессоров PPro, PII и PIII есть инструкции FCOMI, чтобы избежать этой инструкции. Использование FCOMI вместо обычной последовательности 'FCOM / FNSTSW AX / SAHF' сэкономит вам 8 тактов. Поэтому вам следует использовать FCOMI, чтобы избегать FNSTSW везде, где это возможно, даже если это будет стоить дополнительного кода.
На процессорах без инструкции FCOMI обычной практикой сравнения значений с плавающей запятой является:
Код (Text):
FLD [a] FCOMP [b] FSTSW AX SAHF JB ASmallerThanBВы можете улучшить этот код, использовал FNSTSW AX вместо FSTSW AX и протестировав AH напрямую, а не используя неспариваемый SAHF (у TASM 3.0 есть баг, связанный с инструкцией FNSTSW AX):
Код (Text):
FLD [a] FCOMP [b] FNSTSW AX SHR AH,1 JC ASmallerThanBТестирование на ноль или равенство:
Код (Text):
FTST FNSTSW AX AND AH,40H JNZ IsZero ; (флаг нуля инвертирован!)Проверка, больше ли одно значение другого:
Код (Text):
FLD [a] FCOMP [b] FNSTSW AX AND AH,41H JZ AGreaterThanBНе используйте 'TEST AH,41H', так как он не спаривается на PPLain и PMMX.
На PPlain и PMMX инструкция FNSTSW занимает 2 такта, но она вызывает задержку в дополнительные 4 такта после любой инструкции с плавающей запятой, потому что она ждет слово статуса FPU. Этого не происходит после целочисленных инструкций. Вы можете заполнить промежуток между FCOM и FNSTSW целочисленными инструкциями на 4 такта. Спаренный FXCH сразу после FCOM не задерживает FNSTSW, даже если спаривание несовершенно:
Код (Text):
FCOM ; такт 1 FXCH ; такты 1-2 (несовершенное спаривание) INC DWORD PTR [EBX] ; такты 3-5 FNSTSW AX ; такты 6-7Вы можете здесь использовать FCOM вместо FTST, потому что FTST не спаривается. Не забудьте включить N в FNSTSW. У FSTSW (без N) префикс WAIT, который задержит ее в дальнейшем.
Иногда быстрее использовать целочисленные инструкции для сравнения значений с плавающей запятой, как это объяснено в главе 27.6.
26.8 FPREM (все процессоры)
Инструкции FPREM и FPREM1 медленны на всех процессорах. Вы можете заменить их следующим алгоритмом: умножьте на противоположный делитель, получайте дробную часть, получите дробную часть, вычитая усеченное значение, затем умножьте на делитель (смотрите главу 27.5, чтобы узнать, как усекать значения).
Некоторые документы говорят, что эти инструкции могут давать неполную редукцию, и поэтому необходимо повторять инструкции FPREM и FPREM1, пока она не будет сделана. Я протестировал это на нескольких процессорах, начиная со старого 8087, и у меня не было ни одной ситуации, когда потребовалось бы повторение FPREM или FPREM1.
26.9 FRNDINT (все процессоры)
Эта инструкция медленна на всех процессорах. Замените ее следующим:
Код (Text):
FISTP QWORD PTR [TEMP] FILD QWORD PTR [TEMP]Этот код быстрее, несмотря на возможные потери из-за попытки считать [TEMP], когда запись еще не окончена. Рекомендуется поместить какие-нибудь другие инструкции.
26.10 FSCALE и экпоненциальная функция (все процессоры)
FSCALE медленна на всех процессорах. Посчитать целочисленные степени числа 2 можно гораздо быстрее, вставив желаемую степень в поле экспоненты числа с плавающей запятой. Чтобы посчитать 2N, где N - это целое число со знаком, выберите один из примеров ниже, который подходит под границы возможных значений вашего N.
Для |N| < 27-1 вы можете использовать одинарную точность:
Код (Text):
MOV EAX, [N] SHL EAX, 23 ADD EAX, 3F800000H MOV DWORD PTR [TEMP], EAX FLD DWORD PTR [TEMP]Для |N| < 210-1 вы можете использовать двойную точность:
Код (Text):
MOV EAX, [N] SHL EAX, 20 ADD EAX, 3FF00000H MOV DWORD PTR [TEMP], 0 MOV DWORD PTR [TEMP+4], EAX FLD QWORD PTR [TEMP]Для |N| < 214-1 используйте длинную двойную точность:
Код (Text):
MOV EAX, [N] ADD EAX, 00003FFFH MOV DWORD PTR [TEMP], 0 MOV DWORD PTR [TEMP+4], 80000000H MOV DWORD PTR [TEMP+8], EAX FLD TBYTE PTR [TEMP]FSCALE часто используется в вычислениях экспоненциальных функций. Следующий код показывает экспоненциальную функцию без медленных FRNDINT и FSCALE:
Код (Text):
; extern "C" long double _cdecl exp (double x); _exp PROC NEAR PUBLIC _exp FLDL2E FLD QWORD PTR [ESP+4] ; x FMUL ; z = x*log2(e) FIST DWORD PTR [ESP+4] ; round(z) SUB ESP, 12 MOV DWORD PTR [ESP], 0 MOV DWORD PTR [ESP+4], 80000000H FISUB DWORD PTR [ESP+16] ; z - round(z) MOV EAX, [ESP+16] ADD EAX,3FFFH MOV [ESP+8],EAX JLE SHORT UNDERFLOW CMP EAX,8000H JGE SHORT OVERFLOW F2XM1 FLD1 FADD ; 2^(z-round(z)) FLD TBYTE PTR [ESP] ; 2^(round(z)) ADD ESP,12 FMUL ; 2^z = e^x RET UNDERFLOW: FSTP ST FLDZ ; return 0 ADD ESP,12 RET OVERFLOW: PUSH 07F800000H ; +infinity FSTP ST FLD DWORD PTR [ESP] ; return infinity ADD ESP,16 RET _exp ENDP26.11 FPTAN (все процессоры)
Согласно руководствам, FPTAN возвращает два значения X и Y и оставляет на программиста деление Y на X для получения окончательного результата, но фактически она всегда возвращает в X 1, поэтому вы можете сэкономить на делении. Мои тесты показывают, что на всех 32-х битные интеловские процессоры с модулем плавающей запятой или сопроцессором, FPTAN всегда возвращает 1 в X независимо от аргумента. Если вы хотите быть абсолютно уверены, что ваш код будет выполняться корректно на всех процессорах, тогда вы можете протестировать, равен ли X одному, что быстрее, чем деление на X. Значение Y может быть очень высоко, но не бесконечно, поэтому вам не надо тестировать, содержит ли Y правильное число, если вы знаете, что аргумент верен.
26.12 FSQRT (PIII)
Быстрый способ вычислить приблизительное значение квадратного корня на PIII - это умножить обратный корень от x на сам x:
Код (Text):
SQRT(x) = x * RSQRT(x)Инструкция RSQRTSS или RSQRTPS дает обратный корень с точностью 12 бит. Вы можете улучшить точность до 23 бит, используя формулу Ньютона-Рафсона, использованную в интеловской сопроводительной заметке AP-803:
Код (Text):
x0 = RSQRTSS(a) x1 = 0.5 * x0 * (3 - (a * x0)) * x0)где x0 - это первое приближение к обратному корню от a, а x1 - лучшее приближение. Порядок вычисления имеет значение. Вы можете использовать эту формулу до умножения, чтобы получить квадратный корень.
26.13 MOV [MEM], ACCUM (PPlain и PMMX)
Инструкции 'MOV [mem],AL', 'MOV [mem],AX', MOV [mem],EAX расцениваются механизмом спаривания как пишущие в аккумулятор. Поэтому следующие инструкции не спариваются:
Код (Text):
MOV [mydata], EAX MOV EBX, EAXЭта проблема возникает только в короткой версии инструкции MOV, у которой нет базы или индексного регистра и которой может быть только аккумулятор в качестве источника. Вы можете избежать проблему использованием другого регистра, перегруппировкой инструкций, использованием указателя или закодировав общую форму инструкции MOV самостоятельно.
В 32-х битном режиме вы можете записать основную форму 'MOV [mem],EAX' следующим образом:
Код (Text):
DB 89H, 05H DD OFFSET DS:memВ 16-ти битном режиме вы можете записать основную форму MOV [mem],AX' так:
Код (Text):
DB 89H, 06H DW OFFSET DS:memЧтобы использовать AL вместо (E)AX, вам нужно заменить 89H на 88H.
Этот изъян не был исправлен в PMMX.
26.14 Инструкция TEST (PPlain и PMMX)
Инструкция TEST с числовым операндом спаривается только, если назначением являются AL, AX или EAX.
'TEST регистр,регистр' и 'TEST регистр,память' всегда спаривается.
Пример:
Код (Text):
TEST ECX,ECX ; спаривается TEST [mem],EBX ; спаривается TEST EDX,256 ; не спаривается TEST DWORD PTR [EBX],8000H ; не спариваетсяЧтобы сделать их спариваемыми, используйте один из следующих методов:
(Причина этой неспариваемости, вероятно, состоит в том, что первый байт двухбайтной инструкции та же самая, что и для неспариваемых инструкций, и процессор не может проверить второй байт во время проверки спариваемости.)Код (Text):
MOV EAX,[EBX] / TEST EAX,8000H MOV EDX,[EBX] / AND EDX,8000H MOV AL,[EBX+1] / TEST AL,80H MOV AL,[EBX+1] / TEST AL,AL ; (результат в флаге знака)26.15 Битовое сканирование (PPlain и PMMX)
BSF и BSR - хуже всего оптимизированные инструкции на PPlain и PMMX, которые занимают приблизительно 11+2*n тактов, где n равен количеству пропущенных нулей.
Следующий код эмулирует BSR ECX,EAX:
Код (Text):
TEST EAX,EAX JZ SHORT BS1 MOV DWORD PTR [TEMP],EAX MOV DWORD PTR [TEMP+4],0 FILD QWORD PTR [TEMP] FSTP QWORD PTR [TEMP] WAIT ; WAIT требуется только для совместимости со старым 286 ; процессором MOV ECX, DWORD PTR [TEMP+4] SHR ECX,20 ; изолируем экспоненту SUB ECX,3FFH ; снижаем значение TEST EAX,EAX ; очищаем флаг нуля BS1:Следующий код эмулирует BSF ECX,EAX:
Код (Text):
TEST EAX,EAX JZ SHORT BS2 XOR ECX,ECX MOV DWORD PTR [TEMP+4],ECX SUB ECX,EAX AND EAX,ECX MOV DWORD PTR [TEMP],EAX FILD QWORD PTR [TEMP] FSTP QWORD PTR [TEMP] WAIT ; WAIT требуется только для совместимости со старым 286 ; процессором MOV ECX, DWORD PTR [TEMP+4] SHR ECX,20 SUB ECX,3FFH TEST EAX,EAX ; очищаем флаг нуля BS2:Этот код эмуляции не следует использовать PPro, PII и PIII, на которых инструкции битового сканирования занимают только 1 или 2 такта, и где данный код вызовет около двух задежек чтения памяти.
26.16 FLDCW (PPro, PII и PIII)
На PPro, PII и PIII инструкция FLDCW вызывает серьезную задержку, если за ней следует любая инструкция плавающей запятой, считывающая контрольное слово (как делают практически все инструкции плавающей запятой).
Компиляторы C или C++ часто генерируют множество инструкций FLDCW, потому что конвертация чисел с плавающей запятой в целые числа делается с помощью усечения, в то время как другие инструкции плавающей запятой используют округления. После перевода на ассемблер, вы можете улучшить код, использовав округление вместо усечения, где это возможно, или убрав FLDCW из цикла, если требуется усечение внутри него.
Смотрите главу 27.5, чтобы узнать, как сконвертировать число с плавающей запятой в целое без изменения контрольного слова. © Агнер Фог, пер. Aquila
Оптимизация для процессоров семейства Pentium: 26. Проблемные инструкции
Дата публикации 22 авг 2002