Оптимизация для процессоров семейства Pentium: 26. Проблемные инструкции

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

Оптимизация для процессоров семейства 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):
  1.  
  2. IMUL EAX,10

можно заменить на

Код (Text):
  1.  
  2. MOV EBX,EAX / ADD EAX,EAX / SHL EBX,3 / ADD EAX,EBX

или

Код (Text):
  1.  
  2. LEA EAX,[EAX+4*EAX] / ADD EAX,EAX

Умножение чисел с плавающей запятой быстрее, чем целочисленное умножение на PPlain и PMMX, но время, затрачиваемое на преобразование целых чисел в числа с плавающей запятой и обратную конвертацию полученного результата, обычно больше, чем время, сэкономленное в результате использования умножения с плавающей запятой, не считая тех случаев, когда количество конвертаций несравнимо с количеством умножений. Умножение MMX достаточно быстро, но доступно только для 16-ти битных операндов.

26.6 Инструкция WAIT (все процессоры)

Зачастую вы сможете повысить скорость, пренебрегнув инструкцией WAIT. Эта инструкция имеет три функции:

a. Старый процессор 8087 требовали инструкцию WAIT перед каждой инструкцией с плавающей запятой, чтобы убедиться, что сопроцессор готов ее получить.

b. WAIT используется для координирования доступа памяти между модулем вычислений плавающей запятой и модулем целочисленных вычислений.

Примеры:

Код (Text):
  1.  
  2. b.1.  FISTP [mem32]    
  3.       WAIT             ; ждем, пока FPU запишет в память, а потом..
  4.       MOV EAX, [mem32] ; считываем результат модулем целочисленных вычислений
  5.  
  6. b.2.  FILD [mem32]
  7.       WAIT             ; ждем, пока FPU считает значение из памяти..
  8.       MOV [mem32],EAX  ; перед ее перезаписью целым числом
  9.  
  10. b.3.  FLD QWORD PTR [ESP]
  11.       WAIT             ; предотвращаем случайную ошибку от..
  12.       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):
  1.  
  2.     FLD [a]
  3.     FCOMP [b]
  4.     FSTSW AX
  5.     SAHF
  6.     JB ASmallerThanB

Вы можете улучшить этот код, использовал FNSTSW AX вместо FSTSW AX и протестировав AH напрямую, а не используя неспариваемый SAHF (у TASM 3.0 есть баг, связанный с инструкцией FNSTSW AX):

Код (Text):
  1.  
  2.     FLD [a]
  3.     FCOMP [b]
  4.     FNSTSW AX
  5.     SHR AH,1
  6.     JC ASmallerThanB

Тестирование на ноль или равенство:

Код (Text):
  1.  
  2.     FTST
  3.     FNSTSW AX
  4.     AND AH,40H
  5.     JNZ IsZero     ; (флаг нуля инвертирован!)

Проверка, больше ли одно значение другого:

Код (Text):
  1.  
  2.     FLD [a]
  3.     FCOMP [b]
  4.     FNSTSW AX
  5.     AND AH,41H
  6.     JZ AGreaterThanB

Не используйте 'TEST AH,41H', так как он не спаривается на PPLain и PMMX.

На PPlain и PMMX инструкция FNSTSW занимает 2 такта, но она вызывает задержку в дополнительные 4 такта после любой инструкции с плавающей запятой, потому что она ждет слово статуса FPU. Этого не происходит после целочисленных инструкций. Вы можете заполнить промежуток между FCOM и FNSTSW целочисленными инструкциями на 4 такта. Спаренный FXCH сразу после FCOM не задерживает FNSTSW, даже если спаривание несовершенно:

Код (Text):
  1.  
  2.     FCOM                  ; такт 1
  3.     FXCH                  ; такты 1-2 (несовершенное спаривание)
  4.     INC DWORD PTR [EBX]   ; такты 3-5
  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):
  1.  
  2.     FISTP QWORD PTR [TEMP]
  3.     FILD  QWORD PTR [TEMP]

Этот код быстрее, несмотря на возможные потери из-за попытки считать [TEMP], когда запись еще не окончена. Рекомендуется поместить какие-нибудь другие инструкции.

26.10 FSCALE и экпоненциальная функция (все процессоры)

FSCALE медленна на всех процессорах. Посчитать целочисленные степени числа 2 можно гораздо быстрее, вставив желаемую степень в поле экспоненты числа с плавающей запятой. Чтобы посчитать 2N, где N - это целое число со знаком, выберите один из примеров ниже, который подходит под границы возможных значений вашего N.

Для |N| < 27-1 вы можете использовать одинарную точность:

Код (Text):
  1.  
  2.     MOV     EAX, [N]
  3.     SHL     EAX, 23
  4.     ADD     EAX, 3F800000H
  5.     MOV     DWORD PTR [TEMP], EAX
  6.     FLD     DWORD PTR [TEMP]

Для |N| < 210-1 вы можете использовать двойную точность:

Код (Text):
  1.  
  2.     MOV     EAX, [N]
  3.     SHL     EAX, 20
  4.     ADD     EAX, 3FF00000H
  5.     MOV     DWORD PTR [TEMP], 0
  6.     MOV     DWORD PTR [TEMP+4], EAX
  7.     FLD     QWORD PTR [TEMP]

Для |N| < 214-1 используйте длинную двойную точность:

Код (Text):
  1.  
  2.     MOV     EAX, [N]
  3.     ADD     EAX, 00003FFFH
  4.     MOV     DWORD PTR [TEMP],   0
  5.     MOV     DWORD PTR [TEMP+4], 80000000H
  6.     MOV     DWORD PTR [TEMP+8], EAX
  7.     FLD     TBYTE PTR [TEMP]

FSCALE часто используется в вычислениях экспоненциальных функций. Следующий код показывает экспоненциальную функцию без медленных FRNDINT и FSCALE:

Код (Text):
  1.  
  2. ; extern "C" long double _cdecl exp (double x);
  3. _exp    PROC    NEAR
  4. PUBLIC  _exp
  5.         FLDL2E
  6.         FLD     QWORD PTR [ESP+4]             ; x
  7.         FMUL                                  ; z = x*log2(e)
  8.         FIST    DWORD PTR [ESP+4]             ; round(z)
  9.         SUB     ESP, 12
  10.  
  11.         MOV     DWORD PTR [ESP], 0
  12.         MOV     DWORD PTR [ESP+4], 80000000H
  13.         FISUB   DWORD PTR [ESP+16]            ; z - round(z)
  14.         MOV     EAX, [ESP+16]
  15.         ADD     EAX,3FFFH
  16.         MOV     [ESP+8],EAX
  17.         JLE     SHORT UNDERFLOW
  18.         CMP     EAX,8000H
  19.         JGE     SHORT OVERFLOW
  20.         F2XM1
  21.         FLD1
  22.         FADD                                  ; 2^(z-round(z))
  23.         FLD     TBYTE PTR [ESP]               ; 2^(round(z))
  24.  
  25.         ADD     ESP,12
  26.         FMUL                                  ; 2^z = e^x
  27.         RET
  28.  
  29. UNDERFLOW:
  30.         FSTP    ST
  31.         FLDZ                                  ; return 0
  32.         ADD     ESP,12
  33.         RET
  34.  
  35. OVERFLOW:
  36.         PUSH    07F800000H                    ; +infinity
  37.         FSTP    ST
  38.         FLD     DWORD PTR [ESP]               ; return infinity
  39.         ADD     ESP,16
  40.         RET
  41.  
  42. _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:

Код (Text):
  1.  
  2. SQRT(x) = x * RSQRT(x)

Инструкция RSQRTSS или RSQRTPS дает обратный корень с точностью 12 бит. Вы можете улучшить точность до 23 бит, используя формулу Ньютона-Рафсона, использованную в интеловской сопроводительной заметке AP-803:

Код (Text):
  1.  
  2. x0 = RSQRTSS(a)
  3. 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):
  1.  
  2.     MOV [mydata], EAX
  3.     MOV EBX, EAX

Эта проблема возникает только в короткой версии инструкции MOV, у которой нет базы или индексного регистра и которой может быть только аккумулятор в качестве источника. Вы можете избежать проблему использованием другого регистра, перегруппировкой инструкций, использованием указателя или закодировав общую форму инструкции MOV самостоятельно.

В 32-х битном режиме вы можете записать основную форму 'MOV [mem],EAX' следующим образом:

Код (Text):
  1.  
  2.     DB 89H, 05H
  3.  
  4.     DD OFFSET DS:mem

В 16-ти битном режиме вы можете записать основную форму MOV [mem],AX' так:

Код (Text):
  1.  
  2.     DB 89H, 06H
  3.     DW OFFSET DS:mem

Чтобы использовать AL вместо (E)AX, вам нужно заменить 89H на 88H.

Этот изъян не был исправлен в PMMX.

26.14 Инструкция TEST (PPlain и PMMX)

Инструкция TEST с числовым операндом спаривается только, если назначением являются AL, AX или EAX.

'TEST регистр,регистр' и 'TEST регистр,память' всегда спаривается.

Пример:

Код (Text):
  1.  
  2.     TEST ECX,ECX                ; спаривается
  3.     TEST [mem],EBX              ; спаривается
  4.     TEST EDX,256                ; не спаривается
  5.     TEST DWORD PTR [EBX],8000H  ; не спаривается

Чтобы сделать их спариваемыми, используйте один из следующих методов:

Код (Text):
  1.  
  2.     MOV EAX,[EBX] / TEST EAX,8000H
  3.     MOV EDX,[EBX] / AND  EDX,8000H
  4.     MOV AL,[EBX+1] / TEST AL,80H
  5.     MOV AL,[EBX+1] / TEST AL,AL  ; (результат в флаге знака)
(Причина этой неспариваемости, вероятно, состоит в том, что первый байт двухбайтной инструкции та же самая, что и для неспариваемых инструкций, и процессор не может проверить второй байт во время проверки спариваемости.)

26.15 Битовое сканирование (PPlain и PMMX)

BSF и BSR - хуже всего оптимизированные инструкции на PPlain и PMMX, которые занимают приблизительно 11+2*n тактов, где n равен количеству пропущенных нулей.

Следующий код эмулирует BSR ECX,EAX:

Код (Text):
  1.  
  2.         TEST    EAX,EAX
  3.         JZ      SHORT BS1
  4.         MOV     DWORD PTR [TEMP],EAX
  5.         MOV     DWORD PTR [TEMP+4],0
  6.         FILD    QWORD PTR [TEMP]
  7.         FSTP    QWORD PTR [TEMP]
  8.         WAIT    ; WAIT требуется только для совместимости со старым 286
  9.                 ; процессором
  10.  
  11.         MOV     ECX, DWORD PTR [TEMP+4]
  12.         SHR     ECX,20        ; изолируем экспоненту
  13.         SUB     ECX,3FFH      ; снижаем значение
  14.         TEST    EAX,EAX       ; очищаем флаг нуля
  15. BS1:

Следующий код эмулирует BSF ECX,EAX:

Код (Text):
  1.  
  2.         TEST    EAX,EAX
  3.         JZ      SHORT BS2
  4.         XOR     ECX,ECX
  5.         MOV     DWORD PTR [TEMP+4],ECX
  6.         SUB     ECX,EAX
  7.         AND     EAX,ECX
  8.         MOV     DWORD PTR [TEMP],EAX
  9.         FILD    QWORD PTR [TEMP]
  10.  
  11.         FSTP    QWORD PTR [TEMP]
  12.         WAIT    ; WAIT требуется только для совместимости со старым 286
  13.                 ; процессором
  14.  
  15.         MOV     ECX, DWORD PTR [TEMP+4]
  16.         SHR     ECX,20
  17.         SUB     ECX,3FFH
  18.         TEST    EAX,EAX       ; очищаем флаг нуля
  19. BS2:

Этот код эмуляции не следует использовать PPro, PII и PIII, на которых инструкции битового сканирования занимают только 1 или 2 такта, и где данный код вызовет около двух задежек чтения памяти.

26.16 FLDCW (PPro, PII и PIII)

На PPro, PII и PIII инструкция FLDCW вызывает серьезную задержку, если за ней следует любая инструкция плавающей запятой, считывающая контрольное слово (как делают практически все инструкции плавающей запятой).

Компиляторы C или C++ часто генерируют множество инструкций FLDCW, потому что конвертация чисел с плавающей запятой в целые числа делается с помощью усечения, в то время как другие инструкции плавающей запятой используют округления. После перевода на ассемблер, вы можете улучшить код, использовав округление вместо усечения, где это возможно, или убрав FLDCW из цикла, если требуется усечение внутри него.

Смотрите главу 27.5, чтобы узнать, как сконвертировать число с плавающей запятой в целое без изменения контрольного слова. © Агнер Фог, пер. Aquila


0 813
archive

archive
New Member

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