Практическое использование команд MMX для оптимизации программ
Переработано из статьи Дмитрия Сазонова
В данной статье рассказывается о широчайших возможностях применения и использования MMX команд.
Технология MMX состоит из 57-ми совершенно новых инструкций процессора (специально для обработки графики и звука), а также восьми универсальных 64-битных регистров (mm0-mm7). Добавлено четыре новых 64-разрядных типа данных:
Кратко рассмотрим эти команды (более подробное описание можно найти практически в любом современном справочнике по ассемблеру):
Примечание: все процедуры, описанные здесь, основаны на 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 и т.д.), но ассемблерные вставки придется переделывать вручную.