Оптимизация для процессоров семейства Pentium: 27. Специальные темы

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

Оптимизация для процессоров семейства Pentium: 27. Специальные темы — Архив WASM.RU

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

Инструкция LEA полезна для самых разных целей, потому что она умеет делать сдвиг, два сложения и перемещение за один такт.

Пример:

Код (Text):
  1.  
  2. LEA EAX,[EBX+8*ECX-1000]

Гораздо быстрее, чем

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

Инструкцию LEA можно использовать, чтобы делать сложение или сдвиг без изменения флагов. Источник и назначение не обязательно должны быть размером в слово, поэтому 'LEA EAX,[BX]' может стать возможной заменой для 'MOVZX EAX,BX', хотя на многих процессорах это не совсем оптимально.

Как бы то ни было, вам следует знать, что инструкция LEA вызывает задержку AGI на PPlain и PMMX, если она использует базовый или индексный регистр, в которой была произведена запись в предыдущем такте.

Так как инструкция LEA спариваема в V-конвеер на PPlain и PMMX, а инструкции сдвига - нет, вы можете использовать LEA в качестве замены SHL на 1, 2 или 3, если вы хотите, чтобы инструкция выполнялась в V-конвеере.

У 32-х битных конвееров нет документированного режима адресации с только индексным регистром, поэтому инструкция LEA EAX,[EAX*2] на самом деле записывается как 'LEA EAX,[EAX*2+00000000] с 4-х байтовым смещением. Вы можете снизить размер инструкции, написав 'LEA EAX,[EAX+EAX]' или, что еще лучше, 'ADD EAX,EAX'. Последний вариант не приведет к задержке AGI на PPlain и PMMX. Если случилось так, что у вас есть регистр, равный нулю (например, счетчик цикла после последнего прохода), вы можете использовать его как базовый регистр, чтобы снизить размер кода:

Код (Text):
  1.  
  2. LEA EAX,[EBX*4]     ; 7 байтов
  3. LEA EAX,[ECX+EBX*4] ; 3 байтов

27.2 Деление (все процессоры)

Деление отнимает очень много времени. На PPro, PII и PIII целочисленное деление занимает 19, 23 или 39 для байта, слова и двойного слова соответственно. На PPlain и PMMX беззнаковое челочисленное деление занимает приблизительно то же время, хотя деление со знаком занимает немного больше. Поэтому более предпочтительно использовать операнды маленького размера, которые не вызовут переполнения, даже если это будет стоить префикса размера операнда, и использовать по возможности беззнаковое деление.

Целочисленное деление на константу (все процессоры)

Целочисленное деление на степень от двух можно сделать, сдвигая значение вправо. Деление беззнакового целого числа на 2N:

Код (Text):
  1.  
  2.         SHR     EAX, N

Деление целого числа со знаком на 2N:

Код (Text):
  1.  
  2.         CDQ
  3.         AND     EDX, (1 SHL N) -1  ; или SHR EDX, 32-N
  4.         ADD     EAX, EDX
  5.         SAR     EAX, N

Альтернативный SHR короче, чем 'AND if N > 7, но может попасть только в порт 0 (или U-конвеер), в то время как AND может попасть как в порт 0, так и в порт 1 (U- или V-конвеер).

Деление на константу можно сделать на обратное число. Чтобы произвести беззнаковое целочисленное деление q = x / d, вам вначале нужно посчитать число, обратное делителю, f = 2r / d, где r определяет позицию двоично-десятичной точки (точка основания системы счисления). Затем нужно умножить x на f и сдвинуть полученный результат на r позиций вправо. Максимальное значение r равно 32+b, где b равно числу двоичных цифр в d минус 1. (b - это самое большое целое число, для которого 2b <= d). Используйте r = 32+b, чтобы покрыть максильное количество возможных значений делимого x.

Этот метод требует некоторых приемов, чтобы скомпенсировать ошибки округления. Следующий алгоритм даст вам верные результаты для деления беззнакового целого чила с усечением, то есть тот же результат, что дает инструкция DIV (спасибо Terje Mathisen, который изобрел этот метод):

Код (Text):
  1.  
  2.   b = (количество значимых битов в d) - 1
  3.   r = 32 + b
  4.   f = 2r / d
  5.   Если f - целое число, тогда d - это степень от 2: переходим к случаю A.
  6.   Если f - не целое число, тогда проверяем, меньше ли дробная часть f 0.5.
  7.  
  8.   Если дробная часть f &lt; 0.5: переходим к случаю B.
  9.   Если дробная часть f > 0.5: переходим к случаю C.
  10.  
  11.   случай A: (d = 2b)
  12.   результат = x SHR b
  13.  
  14.   случай B: (дробная часть f &lt; 0.5)
  15.   округляем f вниз до ближайшего целого числа
  16.   результат = ((x+1) * f) SHR r
  17.  
  18.   случай C: (дробная часть f > 0.5)
  19.   округляем f вверх до ближайшего целого числа
  20.   результат = (x * f) SHR r

Пример:

Предположите, что вы хотите разделить на 5.

Код (Text):
  1.  
  2. 5 = 00000101b.
  3. b = (количество значимых двоичных чисел) - 1 = 2
  4. r = 32+2 = 34
  5.  
  6. f = 234 / 5 = 3435973836.8 = 0CCCCCCCC.CCC...(hexadecimal)

Дробная часть больше, чем половина: используем случай C. Округляем f вверх до 0CCCCCCCDh.

Следующий код делит EAX на 5 и возвращает результат в EDX:

Код (Text):
  1.  
  2.         MOV     EDX,0CCCCCCCDh
  3.         MUL     EDX
  4.         SHR     EDX,2

После умножения EDX содержит значение, сдвинутое вправо на 32. Так как r = 34, вам нужно сдвинуть еще на 2, чтобы получить окончательный результат. Чтобы поделить на 10, вам нужно всего лишь заменить последнюю строку на 'SHR EDX,3'.

В случае B у вас будет следующее:

Код (Text):
  1.  
  2.         INC     EAX
  3.         MOV     EDX,f
  4.         MUL     EDX
  5.         SHR     EDX,b

Этот код работает для всех значений x, кроме 0FFFFFFFFH, которое дает ноль из-за переполнения в инструкции INC. Если возможно, что x = 0FFFFFFFFH, тогда замените этот код на:

Код (Text):
  1.  
  2.         MOV     EDX,f
  3.         ADD     EAX,1
  4.         JC      DOVERFL
  5.         MUL     EDX
  6. DOVERFL:SHR     EDX,b

Если значение x ограничено, тогда вам следует использовать меньшее значение r, то есть меньшее количество цифр. Может быть несколько причин для того, чтобы сделать это:

  • вы можете установить r = 32 и избежать 'SHR EDX,b' в конце.
  • вы можете установить r = 16+b и использовать инструкции умножения, которые дают 32-х битный результат, вместо 64-х битного. Тогда можно освободить регистр EDX: IMUL EAX,0CCCDh / SHR EAX,18
  • вы можете выбрать значение r, которое будет чаще приводить к случаю C, а не B, чтобы избежать инструкции 'INC EAX'.

Максимальное значение x в этих случаях равно по крайней мере 2r-b, иногда выше. Вы должны делать систематические тесты, если хотите узнать точное максимально значение x, при котором ваш код будет работать корректно.

Вы можете заменить медленную инструкцию умножения более быстрыми инструкциями, как это объяснено в главе 26.5.

Следующий пример делит EAX на 10 и возвращает результат в EAX. Я выбрал r=17, а не 19, потому что это дает код, который легче оптимизировать, и он покрывает такое же количество значений x. f = 217 / 10 = 3333h, случай B: q = (x+1)*3333h:

Код (Text):
  1.  
  2.         LEA     EBX,[EAX+2*EAX+3]
  3.         LEA     ECX,[EAX+2*EAX+3]
  4.         SHL     EBX,4
  5.  
  6.         MOV     EAX,ECX
  7.         SHL     ECX,8
  8.         ADD     EAX,EBX
  9.         SHL     EBX,8
  10.         ADD     EAX,ECX
  11.         ADD     EAX,EBX
  12.         SHR     EAX,17

Проведенные тесты показываеют, что этот код работает правильно для всех значений x < 10004H.

Повторяемое деление целого цисла на одно и то же значение (все процессоры)

Если делитель не известен во время ассемблирования программы, но вы делите на одно и то же число несколько раз, вы тоже можете использовать данный метод. Код должен определить, с каким случаем (A, B и C) он имеет дело, и высчитать f до совершения делений.

Следующий далее код показывает, как делать несколько делений на одно и то же число (беззнаковое деление с усечением). Сначала вызовите SET_DIVISOR, чтобы установить делитель и обратное ему число, затем вызовите DIVIDE_FIXED для каждого значения, которое нужно разделить на один и тот же делитель.

Код (Text):
  1.  
  2. .data
  3.  
  4. RECIPROCAL_DIVISOR DD ?            ; округленное число, обратное делителю
  5. CORRECTION         DD ?            ; случай A: -1, случай B: 1, случай C: 0
  6. BSHIFT             DD ?            ; количество бит в делителе - 1
  7.  
  8. .code
  9.  
  10. SET_DIVISOR PROC NEAR              ; делитель в EAX
  11.         PUSH    EBX
  12.         MOV     EBX,EAX
  13.         BSR     ECX,EAX            ; b = количество бит в делителе - 1
  14.         MOV     EDX,1
  15.         JZ      ERROR              ; ошибка: делитель равен нулю
  16.         SHL     EDX,CL             ; 2^b
  17.         MOV     [BSHIFT],ECX       ; сохраняем b
  18.         CMP     EAX,EDX
  19.         MOV     EAX,0
  20.         JE      SHORT CASE_A       ; делитель - степень от 2
  21.         DIV     EBX                ; 2^(32+b) / d
  22.  
  23.         SHR     EBX,1              ; делитель / 2
  24.         XOR     ECX,ECX
  25.         CMP     EDX,EBX            ; сравниваем остаток с делителем/2
  26.         SETBE   CL                 ; 1 если случай B
  27.         MOV     [CORRECTION],ECX   ; коррекция возможных ошибок округления
  28.         XOR     ECX,1
  29.         ADD     EAX,ECX            ; добавляем 1 если случай C
  30.         MOV     [RECIPROCAL_DIVISOR],EAX ; округленное число, обратное
  31.                                          ; делителю
  32.         POP     EBX
  33.         RET
  34. CASE_A: MOV     [CORRECTION],-1    ; запоминаем, что у нас случай A
  35.  
  36.         POP     EBX
  37.         RET
  38. SET_DIVISOR     ENDP
  39.  
  40. DIVIDE_FIXED PROC NEAR                 ; делимое в EAX, результат в EAX
  41.         MOV     EDX,[CORRECTION]
  42.         MOV     ECX,[BSHIFT]
  43.         TEST    EDX,EDX
  44.         JS      SHORT DSHIFT           ; делитель - степень от 2
  45.         ADD     EAX,EDX                ; коррекция возможных ошибок округления
  46.         JC      SHORT DOVERFL          ; коррекция при переполнении
  47.         MUL     [RECIPROCAL_DIVISOR]   ; умножаем на число, обратное делителю
  48.  
  49.         MOV     EAX,EDX
  50. DSHIFT: SHR     EAX,CL                 ; сдвигаем на количество бит
  51.         RET
  52. DOVERFL:MOV     EAX,[RECIPROCAL_DIVISOR] ; делимое = 0FFFFFFFFH
  53.         SHR     EAX,CL                 ; делаем деление с помощью сдвига
  54.         RET
  55. DIVIDE_FIXED    ENDP

Этот код даст тот же результат, что и инструкция DIV для 0 <= x < 232, 0 < d < 232.

Обратите внимание, что линия 'JC DOVERFL' и ее цель не нужны, если вы уверены, что x < 0FFFFFFFFH.

Если степени от 2 случаются так редко, что не стоит делать специальную оптимизацию из-за них, то вы можете убрать переход на DSHIFT и делать вместо него умножения с CORRECTION = 0 для случая A.

Если делитель меняется так часто, что процедура SET_DIVISOR нуждается в оптимизации, то вы можете заменить инструкцию BSR кодом, который приведен в главе 26.15 для процессоров PPlain и PMMX.

Деление чисел с плавающей запятой (все процессоры)

Деление чисел с плавающей запятой занимает 38 или 39 тактов при самой высокой точности. Вы можете сэкономить время, указав более низкую точность в контрольном слове (на PPlain и PMMX только FDIV и FIDIV более быстры при низкой точности; на PPro, PII и PIII это также относится к FSQRT. Выполнение других инструкций убыстрить этим способом нельзя).

Параллельное деление (PPlain и PMMX)

На PPlain и PMMX можно производить деление числа плавающей запятой и целочисленное деление параллельно. На PPro, PII и PIII это не возможно, потому что целочисленное деление и деление чисел с плавающей запятой используют один и тот же механизм.

Пример: A = A1 / A2; B = B1 / B2

Код (Text):
  1.  
  2.         FILD    [B1]
  3.         FILD    [B2]
  4.         MOV     EAX, [A1]
  5.         MOV     EBX, [A2]
  6.         CDQ
  7.         FDIV
  8.         DIV     EBX
  9.  
  10.         FISTP   [B]
  11.         MOV     [A], EAX

Убедитесь, что вы установили в контрольном слове FPU желаемый метод округления.

Использование обратных инструкций для быстрого деления (PIII)

На PIII вы можете использовать быстрые обратные инструкции RCPSS или PCPPS с делителем, а затем умножить на делимое. Правда, точность будет всего 12 бит. Вы можете повысить ее до 23-х, использовав метод Ньютона-Рафсона, объясненного в интеловской сопроводительной заметке AP-803:

Код (Text):
  1.  
  2. x0 = RCPSS(d)
  3. x1 = x0 * (2 - d * x0) = 2*x0 - d * x0 * x0

где x0 - это первое приближение к обратному от делителя d, а x1 - лучшее приближение. Вы должны использовать эту формулу перед умножение на делимое:

Код (Text):
  1.  
  2.         MOVAPS  XMM1, [DIVISORS]         ; загружаем делители
  3.         RCPPS   XMM0, XMM1               ; приближенное обратное число
  4.         MULPS   XMM1, XMM0               ; формула Ньютона-Рафсона
  5.         MULPS   XMM1, XMM0
  6.  
  7.         ADDPS   XMM0, XMM0
  8.         SUBPS   XMM0, XMM1
  9.         MULPS   XMM0, [DIVIDENDS]        ; результаты в XMM0

Это позволяет сделать 4 деления за 18 тактов с точностью 23 бита. Повышение точность, повторяя формулу Ньютона-Рафсона возможно, но не очень выгодно.

Если вы хотите использовать этот метод для целочисленных делений, тогда вам нужно проверять результаты на ошибки округления. Следующие код делает четыре деления с усечением на упакованных целых числах размером в слово за примерно 42 такта. Это дает точные результаты для 0 <= делимое < 7FFFFH и 0 < делитель lt;= 7FFFFH:

Код (Text):
  1.  
  2.         MOVQ MM1, [DIVISORS]      ; загружаем четыре делителя
  3.         MOVQ MM2, [DIVIDENDS]     ; загружаем четыре делимых
  4.         PUNPCKHWD MM4, MM1        ; распаковываем делители в DWORD'ы
  5.         PSRAD MM4, 16
  6.         PUNPCKLWD MM3, MM1
  7.         PSRAD MM3, 16
  8.         CVTPI2PS XMM1, MM4        ; конвертируем делители в плавающие числа,
  9.                                   ; (два верхних из них)
  10.         MOVLHPS XMM1, XMM1
  11.         CVTPI2PS XMM1, MM3        ; конвертируем нижние два операнда
  12.         PUNPCKHWD MM4, MM2        ; распаковываем делимые в DWORD'ы
  13.  
  14.         PSRAD MM4, 16
  15.         PUNPCKLWD MM3, MM2
  16.         PSRAD MM3, 16
  17.         CVTPI2PS XMM2, MM4        ; конвертируем делимые d плавающие числа
  18.                                   ; (верхние два операнда)
  19.         MOVLHPS XMM2, XMM2
  20.         CVTPI2PS XMM2, MM3        ; конвертируем два нижних операнда
  21.         RCPPS XMM0, XMM1          ; приближенное обратное число делителей
  22.         MULPS XMM1, XMM0          ; улучшаем точность с помощью метода Ньютона-Рафсона
  23.         PCMPEQW MM4, MM4          ; создаем четыре целочисленных единицы за раз
  24.  
  25.         PSRLW MM4, 15
  26.         MULPS XMM1, XMM0
  27.         ADDPS XMM0, XMM0
  28.         SUBPS XMM0, XMM1          ; обратные делители с точностью в 23 бита
  29.         MULPS XMM0, XMM2          ; умножаем на делимые
  30.         CVTTPS2PI MM0, XMM0       ; усекаем нижние два результата
  31.         MOVHLPS XMM0, XMM0
  32.         CVTTPS2PI MM3, XMM0       ; усекаем верхние два результата
  33.         PACKSSDW MM0, MM3         ; упаковываем четыре результата в MM0
  34.         MOVQ MM3, MM1             ; умножаем результаты на делители...
  35.  
  36.         PMULLW MM3, MM0           ; чтобы выявить ошибки округления
  37.         PADDSW MM0, MM4           ; добавляем 1, чтобы скомпенсировать
  38.                                   ; последнее вычитание
  39.         PADDSW MM3, MM1           ; добавляем делитель. он должен быть больше
  40.                                   ; делимого
  41.         PCMPGTW MM3, MM2          ; проверяем, не слишком ли мал
  42.         PADDSW MM0, MM3           ; вычитаем 1, если это не так
  43.         MOVQ [QUOTIENTS], MM0     ; сохраняем четыре результата

Этот код проверяет, не слишком ли мал результат и делает соответствующую коррекцию. Не нужно проверять, если результат слишком велик.

Избегание делений (все процессоры)

Очевидно, что вам минимизировать количество делений. Деления плавающей запятой на константу или повторяющиеся деления на одно и то же значения следуюет делать через умножения на обратное число. Но есть много других ситуаций, когда вы можете снизить количество делений. Например: if (A/B >c) можно переписать как if (A > B*C), если B положительны, и как обратное сравнение, если B отрицательны.

A/B + C/D можно переписать как (A*D + C*B) / (B*D)

Если вы используете целочисленное деление, вам стоит остерегаться того, что ошибки округления могут стать другими после переписывания формул.

27.3 Освобождение регистров FPU (все процессоры)

Вы должны освободить все использованные регистры FPU до выходы из подпрограммы, не считая регистра, использованного для возвращения результата.

Самый быстрые способ освободить один регистр - это FSTP ST. Самый быстрый способ освбодить два регистра на PPlain и PMMX - это FCOMPP, на PPro, PII и PIII вы можете использовать как FCOMPP, так и FSTP ST дважды.

Не рекомендуется использовать FFREE.

27.4 Переход от инструкций FPU к MMX и обратно (PMMX, PII и PIII)

Вы должны выполнить инструкцию EMMS после инструкции MMX, за которой может последовать код с инструкциями FPU.

На PMMX переключение между инструкциями FPU и MMX вызывает высокие потери. Выполнение первой инструкции FPU после EMMS занимает примерно на 58 тактов больше, а первой инструкции MMX после инструкции FPU - на 38 тактов больше.

На PII и PIII подобных потерь нет. Задержку после EMMS можно скрыть, поместив целочисленные инструкции между EMMS и первой инструкции FPU.

27.5 Конвертации чисел с плавающей запятой в целые (все процессоры)

Все подобные конверсии и обратно должны осуществляться через память:

Код (Text):
  1.  
  2.     FISTP DWORD PTR [TEMP]
  3.     MOV EAX, [TEMP]

На PPro, PII и PIII этот код может вызвать потерит из-за попытки считать из [TEMP] до того, как закончена запись туда же, потому что инструкция FIST медленная (глава 17). WAIT не поможет (глава 26.6). Рекомендуется поместить другие инструкции между записью в [TEMP] и чтением оттуда, что бы избежать этих потерь. Это относится ко всем примерам, которые последуют в дальнейшем.

Спецификация языка C и C++ требует, чтобы конверсия из чисел с плавающей запятой в целые числа осуществлялась с помощью усечения, а не округления. Метод, используемый большинством библиотек C, это изменение контрольного слова FPU, чтобы указать инструкции FISTP на усечение, и изменение контрольного слова в прежнее состояние после ее выполнения. Это метод очень медленнен на всех процессорах. На PPro, PII и PIII контрольное слово FPU не может быть переименовано, поэтому все последующие инструкции плавающей запятой будут ждать, пока инструкция FLDCW не будет выведена из обращения.

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

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

Вы можете использовать различные способы, чтобы усекать без изменения контрольного слова, как это показано в примерах ниже. В данных примерах предполагается, что контрольное слово установлено по умолчанию, то есть округление к ближайшему.

Округление к ближайшему

Код (Text):
  1.  
  2. ; extern "C" int round (double x);
  3. _round  PROC    NEAR
  4. PUBLIC  _round
  5.         FLD     QWORD PTR [ESP+4]
  6.         FISTP   DWORD PTR [ESP+4]
  7.         MOV     EAX, DWORD PTR [ESP+4]
  8.         RET
  9. _round  ENDP

Усечение к нулю

Код (Text):
  1.  
  2. ; extern "C" int truncate (double x);
  3. _truncate PROC    NEAR
  4. PUBLIC  _truncate
  5.         FLD     QWORD PTR [ESP+4]   ; x
  6.         SUB     ESP, 12             ; память для локальных переменных
  7.         FIST    DWORD PTR [ESP]     ; округленное значение
  8.         FST     DWORD PTR [ESP+4]   ; значение с плавающей запятой
  9.         FISUB   DWORD PTR [ESP]     ; вычитаем округленное значение
  10.         FSTP    DWORD PTR [ESP+8]   ; разность
  11.         POP     EAX                 ; округленное значение
  12.  
  13.         POP     ECX                 ; значение с плавающей запятой
  14.         POP     EDX                 ; разность (с плавающей запятой)
  15.         TEST    ECX, ECX            ; тестируем знак x
  16.         JS      SHORT NEGATIVE
  17.         ADD     EDX, 7FFFFFFFH      ; устанавливаем флаг переноса, если
  18.                                     ; разность меньше -0
  19.         SBB     EAX, 0              ; вычитаем 1, если x-round(x) &lt; -0
  20.         RET
  21. NEGATIVE:
  22.         XOR     ECX, ECX
  23.         TEST    EDX, EDX
  24.         SETG    CL                  ; 1, если разность > 0
  25.         ADD     EAX, ECX            ; добавляем 1, если x-round(x) > 0
  26.  
  27.         RET
  28. _truncate ENDP

Усечение к минус бесконечности

Код (Text):
  1.  
  2. ; extern "C" int ifloor (double x);
  3. _ifloor PROC    NEAR
  4. PUBLIC  _ifloor
  5.         FLD     QWORD PTR [ESP+4]   ; x
  6.         SUB     ESP, 8              ; память для локальных переменных
  7.         FIST    DWORD PTR [ESP]     ; округленное значение
  8.         FISUB   DWORD PTR [ESP]     ; вычитаем округленное значение
  9.         FSTP    DWORD PTR [ESP+4]   ; разность
  10.         POP     EAX                 ; округленное значение
  11.  
  12.         POP     EDX                 ; разность (с плавающей запятой)
  13.         ADD     EDX, 7FFFFFFFH      ; устанавливаем флаг переноса, если
  14.                                     ; разность меньше -0
  15.         SBB     EAX, 0              ; вычитаем 1, если x-round(x) &lt; -0
  16.         RET
  17. _ifloor ENDP

Эти процедуры работают для -231 < x < 231-1. Они не проверяют на переполнение или NAN'ы.

У PIII есть инструкции для усечения чисел с плавающей запятой одинарной точности: CVTTSS2SI and CVTTPS2PI. Эти инструкции очень полезны, если одинарная точность вас удовлетворяет, но если вы конвертироуете число с более высокой точностью в число с одинарной точностью, чтобы использовать эти инструкции, у вас могут столкнуться с тем, что число может быть округлено вверх во время конверсии.

Альтернатива инструкции FISTP (PPlain и PMMX)

Конвертирование числа с плавающей запятой в целое обычно осуществляется следующим образом:

Код (Text):
  1.  
  2.         FISTP   DWORD PTR [TEMP]
  3.         MOV     EAX, [TEMP]

Альтернативный метод заключает в:

Код (Text):
  1.  
  2. .DATA
  3. ALIGN 8
  4. TEMP    DQ      ?
  5. MAGIC   DD      59C00000H   ; FPU-представление 2^51 + 2^52
  6.  
  7. .CODE
  8.         FADD    [MAGIC]
  9.         FSTP    QWORD PTR [TEMP]
  10.         MOV     EAX, DWORD PTR [TEMP]

Добавление 'волшебного числа' 251+252 есть такой эффект, что любое целое число между -231 и +231 будет выравнено в нижних 32-х битах, когда сохраняется как число с плавающей запятой двойной точности. Результат будет такой же, какой бы вы получили с помощью инструкции FISTP со всеми методами окруления, кроме усечения к нулю. Результат будет отличаться от FISTP, если в контрольном слове задано усечение или в случае переполнения. Вам может потребоваться инструкция WAIT для совместимости со старым 80287 процессором (глава 26.6)

Этот метод не быстрее использования FISTP, но он дает большую гибкость на PPlain и PMMX, потому между FADD и FSTP 3 такта, которые можно заполнить другими инструкциями. Вы можете умножить или разделить число на степень от друх в той же операции, сделав обратно по отношению к магическому числу. Вы также можете добавить константу, добавив ее к магическому числу, которое тогда будет иметь двойную точность.

27.6 Использование целочисленных инструкция для осуществления операций плавающей запятой (все процессоры)

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

Пример:

Код (Text):
  1.  
  2. FLD QWORD PTR [ESI] / FSTP QWORD PTR [EDI]

Заменить на:

Код (Text):
  1.  
  2. MOV EAX,[ESI] / MOV EBX,[ESI+4] / MOV [EDI],EAX / MOV [EDI+4],EBX

Тестируем, не равно ли значение с плавающей запятой нулю:

Значение с плавающей запятой, равное нулю, обычно представляется как 32 или 64 обнуленных бита, но здесь есть один подводные камень: бит знака может быть равен нулю! Минус ноль считается правильным числом с плавающей запятой, и процессор может сгенерировать ноль с уставноленным битом знака, если, например, отрицательное число было умножено на ноль. Поэтому если вы хотите узнать, не равно ли число с плавающей запятой нулю, вам не следует тестировать бит знака.

Пример:

Код (Text):
  1.  
  2. FLD DWORD PTR [EBX] / FTST / FNSTSW AX / AND AH,40H / JNZ IsZero

Используйте целочисленные инструкции вместо этого и сдвиньте бит знака:

Код (Text):
  1.  
  2. MOV EAX,[EBX] / ADD EAX,EAX / JZ IsZero

Если число с плавающей запятой имеет двойную точность (QWORD), тогда вам нужно протестировать только биты 32-62. Если они равны нулю, тогда нижняя половина будет также равна нулю, если это верное число с плавающей запятой.

Тест на то, отрицательно ли значение:

Число с плавающей запятой отрицательно, если бит знака и установлен по крайней мере один бит.

Пример:

Код (Text):
  1.  
  2. MOV EAX,[NumberToTest] / CMP EAX,80000000H / JA IsNegative

Манипулирование битом знака:

Вы можете изменить знак числа с плавающей запятой просто инвертирововав бит знака:

Пример:

Код (Text):
  1.  
  2. XOR BYTE PTR [a] + (TYPE a) - 1, 80H

Похожим образом вы можете получить асбсолютное значение числа с плавающей запятой, просто сANDив бит знака:

Сравнивание чисел:

Числа с плавающей запятой сохраняются в особом формате, который позволяет использовать целочисленные инструкции для сравнения чисел с плавающей запятой, не считая бита знака. Если вы уверены, что оба сравниваемые числа с плавающей запятой являются правильными и положительными, вы можете простой сравнить их как целые:

Пример:

Код (Text):
  1.  
  2. FLD [a] / FCOMP [b] / FNSTSW AX / AND AH,1 / JNZ ASmallerThanB

Изменить на:

Код (Text):
  1.  
  2. MOV EAX,[a] / MOV EBX,[b] / CMP EAX,EBX / JB ASmallerThanB

Этот метод работает только, если у двух чисел одна и та же точность, и вы уверены, что ни у одного из числа не установлен бит знака.

Если возможны отрицательные числа, вы можете их сконвертировать определенным образом и сделать знаковое сравнение:

Код (Text):
  1.  
  2.         MOV     EAX, [a]
  3.  
  4.         MOV     EBX, [b]
  5.         MOV     ECX, EAX
  6.         MOV     EDX, EBX
  7.         SAR     ECX, 31              ; скопировать бит знака
  8.         AND     EAX, 7FFFFFFFH       ; убрать бит знака
  9.         SAR     EDX, 31
  10.         AND     EBX, 7FFFFFFFH
  11.         XOR     EAX, ECX      ; преобразуем, если установлен бит знака
  12.         XOR     EBX, EDX
  13.         SUB     EAX, ECX
  14.         SUB     EBX, EDX
  15.         CMP     EAX, EBX
  16.         JL      ASmallerThanB        ; знаковое сравнение

Этот метод работает для всех правильных чисел с плавающей запятой, включая -0.

27.7 Использование инструкции с плавающей запятой, чтобы осуществлять целочисленные операции (PPlain и PMMX)

Целочисленное умножение (PPlain и PMMX)

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

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

Целочисленное умножение быстрее, чем умножение с плавающей запятой на PPro, PII и PIII.

Целочисленное деление (PPlain и PMMX)

Деление плавающей запятой не быстрее, чем целочисленное деление, но вы можете делать другие целочисленные операции (включая целочисленное деление, но не целочисленное умножение), в то время как работает FPU на деление плавающей запятой (смотри пример выше).

Конвертирование двоичных чисел в десятичные (все процессоры)

Использование инструкции FBSTP - простой и удобный путь для того, чтобы конвертировать двоичные числа в десятичные, хотя не обязательно самый быстрый метод.

27.8 Перемещение блоков данных (все процессоры)

Есть несколько способов перемещения блоков данных. Наиболее общий метод - это REP MOVSD, но при определенных условиях другие методы быстрее.

На PPlain и PMMX быстрее переместить 8 байтов за раз, если место назначения не находится в кэше:

Код (Text):
  1.  
  2. TOP:    FILD    QWORD PTR [ESI]
  3.         FILD    QWORD PTR [ESI+8]
  4.         FXCH
  5.         FISTP   QWORD PTR [EDI]
  6.         FISTP   QWORD PTR [EDI+8]
  7.         ADD     ESI, 16
  8.         ADD     EDI, 16
  9.         DEC     ECX
  10.         JNZ     TOP

Источник и место назначения должны быть выравнены на 8. Дополнительное время, используемое медленными инструкциями FILD и FISTP компенсируется тем, что вам требуется сделать в два раза меньше операций записывания. Обратите внимание, что этот метод имеет преимущество только на PPlain и PMMX и только тогда, когда место назначения не находится в кэше первого уровня. Вы не можете использовать FLD и FSTP (без I) с противоположными последовательностями битов, потому что ненормальные числа обрабатываются медленно и не гарантируется, что они останутся неизмененными.

На PMMX, если назначение не находится в кэше, быстрее использовать инструкции MMX для перемещения восьми байтов за раз, если место назначения не находится в кэше.

Код (Text):
  1.  
  2. TOP:    MOVQ    MM0,[ESI]
  3.         MOVQ    [EDI],MM0
  4.         ADD     ESI,8
  5.         ADD     EDI,8
  6.         DEC     ECX
  7.         JNZ     TOP

Данный цикл не нужно оптимизировать или разворачивать, если ожидаются промахи кэша, потому что здесь узкое место - это доступ к памяти, а не выполнение инструкций.

На процессорах PPro, PII и PIII инструкция REP MOVSD особенно быстра, если соблюдены следующие условия:

  • источник и назначение должны быть выравнены на 8
  • направление должно быть вперед (очищен флаг направления)
  • счетчик (ECX) должен быть больше или равен 64
  • разность между EDI и ESI должна быть больше или равна 32

На PII быстрее использовать регистры MMX, если вышеприведенные условия не соблюдены и место назначения находится в кэше первого уровня. Цикл можно развернуть в два раза, а источник и назначение должны быть выравнены на 8.

На PIII самый быстрый путь перемещения данных - это использовать инструкцию MOVAPS, если вышеприведенные условия не соблюдены или если место назначения не находится в кэше первого или второго уровня:

Код (Text):
  1.  
  2.         SUB     EDI, ESI
  3. TOP:    MOVAPS  XMM0, [ESI]
  4.         MOVAPS  [ESI+EDI], XMM0
  5.         ADD     ESI, 16
  6.         DEC     ECX
  7.         JNZ     TOP

В отличии от FLD, MOVAPS может обрабатывать любую пследовательность битов без всяких проблем. Помните, что источник и назначение должны быть выравнены на 16.

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

На PIII у вас также есть опция прямой записи в RAM-память без вовлечения кэша, используя инструкцию MOVNTQ или MOVNTPS. Это может быть полезным, если вы не хотите, чтобы место назначение попало в кэш. MOVNTPS чуть-чуть быстрее, чем MOVNTQ.

27.9 Самомодифицирующийся код (все процессоры)

Потери при выполнении кода сразу после того, как тот был изменен, занимают примерно 19 тактов на PPlain, 31 на PMMX и 150-300 на PPro, PII и PIII. Процессоры 80486 и более ранние требуют переход между модифицирующим и модифицируемым кодом, чтобы очистить кэш кода.

Чтобы получить разрешение на модифицирование кода в защищенное операционной системе, вам потребуется вызвать специальные системные функции: в 16-битной Windows это ChangeSelector, в 32-х битной Windows - VirtualProtect и FlushInstructionCache (или поместить код в сегмент данных).

Самомодифицирующийся код не считается хорошим тоном программирования, но можно пойти на его применение, если выигрыш в скорости значителен.

27.10 Определение типа процессора (все процессоры)

Я думаю, что теперь достаточно очевидно, что оптимальное для одного процессора может не являться таковым для другого. Вы можете сделать несколько вариантов наиболее критичных участков кода вашей программы, чтобы они выполнялись максимально быстро на любом процессоре. Однако вам потребуется определить, на каком процессоре программа выполняется в настоящий момент. Если вы используете инструкции, которые не поддерживаются всеми процессорами, т.е. условные перемещения, FCOMI, инструкции MMX и XMM), то вы можете сначала проверить, поддерживает ли процессор данные инструкции. Процедура, приведенная ниже, проверяет тип процессора и поддерживаемые им технологии.

Код (Text):
  1.  
  2. ; задаем инструкцию CPUID, если она не известна ассемблеру:
  3. CPUID   MACRO
  4.         DB      0FH, 0A2H
  5. ENDM
  6.  
  7. ; Прототип С++:
  8. ; extern "C" long int DetectProcessor (void);
  9.  
  10. ; возвращаемое значение:
  11. ; bits 8-11 = семья (5 для PPlain и PMMX, 6 для PPro, PII и PIII)
  12. ; bit  0 = поддерживаются инструкции FPU
  13. ; bit 15 = поддерживаются условные переходы и инструкция FCOMI
  14. ; bit 23 = поддерживаются инструкции MMX
  15. ; bit 25 = поддерживаются инструкции XMM
  16.  
  17. _DetectProcessor PROC NEAR
  18.  
  19. PUBLIC  _DetectProcessor
  20.         PUSH    EBX
  21.         PUSH    ESI
  22.         PUSH    EDI
  23.         PUSH    EBP
  24.         ; определяем, поддерживает ли микропроцессор инструкцию CPUID
  25.         PUSHFD
  26.         POP     EAX
  27.         MOV     EBX, EAX
  28.         XOR     EAX, 1 SHL 21    ; проверяем, можно ли изменять бит CPUID
  29.         PUSH    EAX
  30.         POPFD
  31.         PUSHFD
  32.         POP     EAX
  33.         XOR     EAX, EBX
  34.         AND     EAX, 1 SHL 21
  35.         JZ      SHORT DPEND      ; инструкция CPUID не поддерживается
  36.  
  37.         XOR     EAX, EAX
  38.         CPUID                    ; получаем количество функций CPUID
  39.         TEST    EAX, EAX
  40.         JZ      SHORT DPEND      ; функция 1 CPUID не поддерживается
  41.         MOV     EAX, 1
  42.         CPUID                    ; получаем семью и особенности процессора
  43.         AND     EAX, 000000F00H  ; семья
  44.         AND     EDX, 0FFFFF0FFH  ; флаги особенностей
  45.         OR      EAX, EDX         ; комбинируем биты
  46. DPEND:  POP     EBP
  47.         POP     EDI
  48.         POP     ESI
  49.         POP     EBX
  50.  
  51.         RET
  52. _DetectProcessor ENDP

Обратите внимание, что некоторые операционные системы не позволяют использовать инструкции XMM. Информация о том, как узнать, поддерживает ли операционная система инструкции XMM, можно найти в интеловской инструкции AP-900: "Identifying support for Streaming SIMD Extensions in the Processor and Operating System". Больше информации о идентификации процессора можно найти в инструкции AP-485: "Intel Processor Identification and the CPUID Instruction".

Если ассемблер не поддерживает инструкции MMX, XMM, условного перемещения данных, можно использовать специальные макросы (www.agner.org/assem/macros.zip) © Агнер Фог, пер. Aquila


0 879
archive

archive
New Member

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