allasm.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.

Пример:

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, чтобы узнать, как сконвертировать число с плавающей запятой в целое без изменения контрольного слова.