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:
При этих условиях количество мопов будет примерно равно 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.
Пример:
IMUL EAX,10
можно заменить на
MOV EBX,EAX / ADD EAX,EAX / SHL EBX,3 / ADD EAX,EBX
или
LEA EAX,[EAX+4*EAX] / ADD EAX,EAX
Умножение чисел с плавающей запятой быстрее, чем целочисленное умножение на PPlain и PMMX, но время, затрачиваемое на преобразование целых чисел в числа с плавающей запятой и обратную конвертацию полученного результата, обычно больше, чем время, сэкономленное в результате использования умножения с плавающей запятой, не считая тех случаев, когда количество конвертаций несравнимо с количеством умножений. Умножение MMX достаточно быстро, но доступно только для 16-ти битных операндов.
26.6 Инструкция WAIT (все процессоры)
Зачастую вы сможете повысить скорость, пренебрегнув инструкцией WAIT. Эта инструкция имеет три функции:
a. Старый процессор 8087 требовали инструкцию WAIT перед каждой инструкцией с плавающей запятой, чтобы убедиться, что сопроцессор готов ее получить.
b. WAIT используется для координирования доступа памяти между модулем вычислений плавающей запятой и модулем целочисленных вычислений.
Примеры:
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 обычной практикой сравнения значений с плавающей запятой является:
FLD [a]
FCOMP [b]
FSTSW AX
SAHF
JB ASmallerThanB
Вы можете улучшить этот код, использовал FNSTSW AX вместо FSTSW AX и протестировав AH напрямую, а не используя неспариваемый SAHF (у TASM 3.0 есть баг, связанный с инструкцией FNSTSW AX):
FLD [a]
FCOMP [b]
FNSTSW AX
SHR AH,1
JC ASmallerThanB
Тестирование на ноль или равенство:
FTST
FNSTSW AX
AND AH,40H
JNZ IsZero ; (флаг нуля инвертирован!)
Проверка, больше ли одно значение другого:
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, даже если спаривание несовершенно:
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 (все процессоры)
Эта инструкция медленна на всех процессорах. Замените ее следующим:
FISTP QWORD PTR [TEMP]
FILD QWORD PTR [TEMP]
Этот код быстрее, несмотря на возможные потери из-за попытки считать [TEMP], когда запись еще не окончена. Рекомендуется поместить какие-нибудь другие инструкции.
26.10 FSCALE и экпоненциальная функция (все процессоры)
FSCALE медленна на всех процессорах. Посчитать целочисленные степени числа 2 можно гораздо быстрее, вставив желаемую степень в поле экспоненты числа с плавающей запятой. Чтобы посчитать 2N, где N - это целое число со знаком, выберите один из примеров ниже, который подходит под границы возможных значений вашего N.
Для |N| < 27-1 вы можете использовать одинарную точность:
MOV EAX, [N]
SHL EAX, 23
ADD EAX, 3F800000H
MOV DWORD PTR [TEMP], EAX
FLD DWORD PTR [TEMP]
Для |N| < 210-1 вы можете использовать двойную точность:
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 используйте длинную двойную точность:
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:
; 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 ENDP
26.11 FPTAN (все процессоры)
Согласно руководствам, FPTAN возвращает два значения X и Y и оставляет на программиста деление Y на X для получения окончательного результата, но фактически она всегда возвращает в X 1, поэтому вы можете сэкономить на делении. Мои тесты показывают, что на всех 32-х битные интеловские процессоры с модулем плавающей запятой или сопроцессором, FPTAN всегда возвращает 1 в X независимо от аргумента. Если вы хотите быть абсолютно уверены, что ваш код будет выполняться корректно на всех процессорах, тогда вы можете протестировать, равен ли X одному, что быстрее, чем деление на X. Значение Y может быть очень высоко, но не бесконечно, поэтому вам не надо тестировать, содержит ли Y правильное число, если вы знаете, что аргумент верен.
26.12 FSQRT (PIII)
Быстрый способ вычислить приблизительное значение квадратного корня на PIII - это умножить обратный корень от x на сам x:
SQRT(x) = x * RSQRT(x)
Инструкция RSQRTSS или RSQRTPS дает обратный корень с точностью 12 бит. Вы можете улучшить точность до 23 бит, используя формулу Ньютона-Рафсона, использованную в интеловской сопроводительной заметке AP-803:
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 расцениваются механизмом спаривания как пишущие в аккумулятор. Поэтому следующие инструкции не спариваются:
MOV [mydata], EAX
MOV EBX, EAX
Эта проблема возникает только в короткой версии инструкции MOV, у которой нет базы или индексного регистра и которой может быть только аккумулятор в качестве источника. Вы можете избежать проблему использованием другого регистра, перегруппировкой инструкций, использованием указателя или закодировав общую форму инструкции MOV самостоятельно.
В 32-х битном режиме вы можете записать основную форму 'MOV [mem],EAX' следующим образом:
DB 89H, 05H
DD OFFSET DS:mem
В 16-ти битном режиме вы можете записать основную форму MOV [mem],AX' так:
DB 89H, 06H
DW OFFSET DS:mem
Чтобы использовать AL вместо (E)AX, вам нужно заменить 89H на 88H.
Этот изъян не был исправлен в PMMX.
26.14 Инструкция TEST (PPlain и PMMX)
Инструкция TEST с числовым операндом спаривается только, если назначением являются AL, AX или EAX.
'TEST регистр,регистр' и 'TEST регистр,память' всегда спаривается.
Пример:
TEST ECX,ECX ; спаривается
TEST [mem],EBX ; спаривается
TEST EDX,256 ; не спаривается
TEST DWORD PTR [EBX],8000H ; не спаривается
Чтобы сделать их спариваемыми, используйте один из следующих методов:
(Причина этой неспариваемости, вероятно, состоит в том, что первый байт
двухбайтной инструкции та же самая, что и для неспариваемых инструкций, и
процессор не может проверить второй байт во время проверки спариваемости.)
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:
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:
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, чтобы узнать, как сконвертировать число с плавающей запятой в целое без изменения контрольного слова.
[C] Агнер Фог, пер. Aquila