Практическое использование команд MMX для оптимизации программ

Переработано из статьи Дмитрия Сазонова

В данной статье рассказывается о широчайших возможностях применения и использования MMX команд.

Технология MMX состоит из 57-ми совершенно новых инструкций процессора (специально для обработки графики и звука), а также восьми универсальных 64-битных регистров (mm0-mm7). Добавлено четыре новых 64-разрядных типа данных:

Кратко рассмотрим эти команды (более подробное описание можно найти практически в любом современном справочнике по ассемблеру):

  1. Команды пересылки данных - обмен данными между: MMX регистрами, стандартными 32-битными регистрами, а также ячейками памяти. (Movd, Movq)
  2. Команды преобразования типов - конверсия из одного типа данных MMX в другой (Packss, Packus, Punpckh, Punpckl)
  3. Арифметические операции - универсальные команды сложения, вычитания, умножения упакованных типов данных; как с насыщением (т.е при переполнении остается максимальное или минимальное значение), так и без него. (Padd, Padds, Paddus, Psub, Psubs, Psubus, Pmulhw, Pmullw, Pmaddwd)
  4. Команды сравнения - сравнивают элементы данных (байты, слова, двойные слова) с последующим созданием маски результата. (Pcmpeq, Pcmpgt)
  5. Логические операции - обычные команды логики, за исключением, что работают с 64-битной точностью (Pxor, Por, Pand, Pandn).
  6. Команды сдвига - аналог стандартных команд сдвига, только эти предназначены специально для работы с упакованными типами данных. (Psll, Psrl, Psra).
  7. Обнуление FPU регистров (Emms) - обязательно должно стоять после всех MMX процедур, т.к регистры mm0-mm7 заодно являются мантиссой регистров математического сопроцессора (st0-st7).

Примечание: все процедуры, описанные здесь, основаны на 32-(24-)-битных RGB-режимах графики.

1. Для первого примера возьмем реализацию быстрого вывода картинки (спрайта) с наложением - байты изображения складываются с уже имеющимися байтами на экране (источники освещения[lensflares], частицы [particles], различные многослойные эффекты и т.д.):

Edi - адрес экрана (буфера экрана)
Ebx - адрес спрайта
Mov al, [edi]; загрузка байта с экрана
Mov ah, [ebx]; загрузка байта спрайта
Add al,ah; их сложение
Jnc loop; если число получилось меньше 255, то переход на loop
Mov al,255; при числе >255 делаем его равным 255
loop: Mov [edi],al; поместить содержимое регистра al на экран
Inc edi; следующий байт экрана
Int ebx; следующий байт спрайта
....;
....; (так 3 раза, т.е отдельно для каждой R,G,B-компоненты)

попробуем переделать это под MMX:

Movd mm0,[edi]; загрузка четырех байт с экрана
Movd mm1,[ebx]; загрузка четырех байт спрайта
Paddusb mm0,mm1; сложение их с проверкой переполнения на 255 (сразу четыре байта !!!)
Movd [edi],mm0; поместить эти четыре байта обратно на экран
Add ebx,4; следующие байты спрайта
Add edi,4; следующие байты экрана

Сразу очевиден огромный прирост в скорости из-за параллельной обработки сразу четырех байт, а также из-за отсутствия команд условных переходов. Кстати - при использовании Psubusb (с константой) вместо Paddusb можно получить супер-быстрый RGB-фильтр, что тоже весьма приятно.

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

Edi - адрес экрана (буфера экрана)
Ebx –
адрес спрайта

Mov al,[ebx] ;
загрузка байта спрайта
Cmp al,0 ;
проверка на прозрачность
; (т.е. при al=0 на экран байт не записываем)
Jz loop
Mov [edi],al ;
если al<>0 - кладем его на экран
loop: Inc ebx ;
следующий байт спрайта
Inc edi ;
следующий байт экрана
....
....
;(так 3 раза, т.е отдельно для каждой R,G,B-компоненты)

MMX вариант:

Pxor mm2,mm2 ; обнуляем регистр mm2
Movd mm0,[ebx] ;
загрузка четырех байт спрайта 
Movd mm1,[esi] ;
загрузка четырех байт экрана (фона)
Pcmpeqb mm2,mm0 ;
делаем маску тех байт которые надо вывести
Pand mm2,mm1 ;
обнуляем ненужные
Por mm1,mm0 ;
складываем их с нашими байтами спрайта
Movd [esi],mm1 ;
кладем их обратно на экран
Add ebx,4
Add esi,4

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

3. Размытие при движении (motion blur) – объекты оставляют за собой плавно угасающий шлейф (применяется и на телевидении, и в 3D играх).

mask7f1 = 0x7f7f7f7f7f7f7f7f;
movq mm4,mask7f1 ; загрузка маски в регистр mm4

movq mm0,[esi] ; загрузка восьми байт текущего кадра
; (изображение на экране в данный момент)
movq mm1,[edi] ; загрузка восьми байт предыдущего кадра
psrlq mm0,1 ; быстрая процедура деления на два каждого из 8 байт
psrlq mm1,1 ; (как в mm0, так и в mm1)
pand mm0,mm4 ; -//-
pand mm1,mm4 ; -//-
paddb mm0,mm1 ; сложение mm0 с mm1(=среднее арифметическое между mm0 и mm1)
movq [esi],mm0 ; кладем это на экран (в видеобуфер)
movq [edi],mm0 ; а также в буфер предыдущего кадра
add esi,8
add edi,8

Сразу восемь байт! То самое 400% ускорение, которое нам так долго рекламировала Intel.

4. Канал прозрачности (alpha blending) [далее ab] – одно изображение плавно появляется или растворяется поверх другого (также очень часто применяется при работе с видео).

Формула ab:a = b + (a - b) * alpha

Где: a – основное изображение
b – изображение которое накладывается
alpha – количество градаций(позиций) ab - обычно хватает 0...255.

переведем это все в MMX-команды:

a) инициализация регистров

биты31...2423...1615...8 7...0
eax = alpha alpha alpha alpha

movd mm4,eax; переносим данных из eax в mm4
punpcklwd mm4,mm4 ; создаем четыре cлова alpha-канала
psrlw mm4,8 ;
переносим старшую часть слов в младшую
pxor mm7,mm7 ;
обнуляем mm7

б) основная процедура

movd mm0,[esi] ; загрузить в mm1 четыре байта накладываемого изображения
movd mm1,[edx] ;
загрузить в mm0 четыре байта основного изображения
punpcklbw mm0,mm7 
punpcklbw mm1,mm7 
psubw mm0,mm1 ;
вычитаем из накладываемого, основное изображение
psllw mm1,8 ;
переносим младшую часть слов регистра mm1 в cтаршую
pmullw mm0,mm4 ;
умножаем накладываемое изображение на alpha-кана
paddw mm1,mm0 ;
складываем его с основным
psrlw mm1,8 ;
переносим результат из старшей части слова в младшую
packuswb mm1,mm1 ;
и переводим его в 32 бита
movd [edi],mm1 ;
кладем полученное изображение на экран (в видеобуфер)
add esi,4
add edx,4
add edi,4

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

Ну вот; в приведенных примерах мы рассмотрели практически все MMX-команды и способы ММХ-оптимизации - применяя их. Осталось заметить: чтобы получить еще более быстродействующие программы, нужно размещать команды согласно правилам оптимизации под соответствующий процессор (к сожалению, у Intel и AMD они разные), а также правильно использовать его внутренний кэш. Большую часть этой работы берут на себя компиляторы высокого уровня (C++, Pascal, Basic и т.д.), но ассемблерные вставки придется переделывать вручную.