Показаны сообщения с ярлыком оптимизация. Показать все сообщения
Показаны сообщения с ярлыком оптимизация. Показать все сообщения

четверг, 18 декабря 2008 г.

Про оптимизацию компилятором

Пытаюсь написать несколько функций для работы с картинками. Понятно дело пытаюсь, оптимизировать, собственно к этому два замечания.

Сравнение оптимизации компиляторами MSVS 2003 и MSVS 2005


Один и тот же код (функция масштабирования ) компилирую на MSVS 2003 и MSVS 2005 (на 2008 не пробовал). Получаем вот такие графики скоростей (под скоростью здесь понимается размер результирующей картинки поделенный на время работы функции):



Что за всплески в точках меньших 1.0 - не знаю, пока не разбирался. В 1.0 и выше, критичная часть кода уже на ассемблере и компилятор роли не играет. Но очевидно, что народ в Микрософт не сидит сиднем и компиляторы оптимизируют код все лучше (хотя ручной настройкой можно добится существенных результатов)

Директива __forceinline


Директива __forceinline прекрасная вещь. Написал код все того же масштабирования померил время - получается отлично (я для сравнения использовал такие же функции из интеловской либы IPP - моя получилось в полтора раза быстрее, что меня вполне устраивало). Решил немного пооптимизировать код, в том числе одну из внутренних функций наградил директивой __forceinline, которая там смотрелась вполне к месту. Замерил время. Увеличилось в полтора, два раза. Так что дурное использование __forceinline может дать результаты обратные тем, которые предполагались.

воскресенье, 14 декабря 2008 г.

Про оптимизацию функции memset используя SSE

Продолжаю интересоваться возможностями SIMD команд в плане оптимизации своего кода. Сегодня поговорим, про заполнение памяти и соответственно функцию memset.

Функция memset, как известно, заполняет область памяти переданным байтом, соответственно на вход получает указатель на эту самую область и сам байт. Поскольку задача оптимизировать функцию с использованием SSE команд, писать мы будем на ассемблере. А значит, параметры функции будем передавать в регистрах процессора. Договоримся следующим образом. Указатель на память, которую необходимо заполнить передаем в регистре EDI, размер памяти (в байтах) – в ECX, а значение которым будем заполнять - в EAX.

Чтобы работать с ассемблером непосредственно в Visual C++ рекомендую, кроме служебного слова _asm, посмотреть в MSDN, что означает вот такая конструкция __declspec(naked). Местами это сильно облегчит жизнь.

Стандартный вариант функции memset выглядит следующим образом

mov         bl,al 
mov bh,bl
mov eax,ebx
shl eax,10h
mov ax,bx
mov ebx, ecx
shr ecx, 2
rep stosd
mov ecx,ebx
and ecx,3
rep stosb


Наша задача ускорить его, используя SSE команды.

Но прежде чем предложить измененный вариант, необходимо поговорить о том, как мы будем осуществлять сравнение скоростей. Я набросал простенькое консольное приложение, которое выделяло память, вызывало 1000 раз функцию и замеряло время. Размеры блоков варьировались от 50 байт до 10 мегабайт.

Когда мы используем SSE, следует учитывать, что память, с которой мы работаем, должна быть выровнена к 16 байтам (иначе пересылка содержимого регистров XMM в память будет крайне медленной). Поэтому работа новой функции разбивается на три этапа. Первый заполняем память командой rep stosb, до того момента пока указатель не будет кратен 16, затем помещаем во все байты регистра XMM0, и в цикле копируем его в память. И, наконец, вновь при помощи rep stosb заполняем остаток. На первом и последнем шаге размер буфера будет от 0 и до 15 байт.

Выглядит эта новая функция, следующим образом:

 test ecx, ecx
jz func_exit

cmp ecx, 0x1F
jge sse_part

mov bl,al
mov bh,bl
mov eax,ebx
shl eax,10h
mov ax,bx
mov ebx, ecx
shr ecx, 2
rep stosd
mov ecx,ebx
and ecx,3
rep stosb
jmp func_exit
sse_part:
mov ebx, edi
and ebx, 0x0F
jz xmm_align

xor bl, 0x0F
inc ebx
sub ecx, ebx

loop_byte_2:
mov [edi], al
inc edi
dec ebx
jnz loop_byte_2

xmm_align:
pxor xmm0, xmm0
movd xmm0, eax
punpcklwd xmm0, xmm0
pshufd xmm0, xmm0, 0
movdqa xmm1, xmm0
psllw xmm1, 8
por xmm0, xmm1
mov ebx, ecx
and ebx, 0x0F
shr ecx, 4

loop_xmm:
movntdq [edi], xmm0
add edi, 0x10
dec ecx
jnz loop_xmm

mov ecx, ebx
rep stosb

func_exit:
sfence
emms
ret


Итак, и первый и второй вариант прекрасно работают. Теперь про интересное, а именно посмотрим на график скорости в зависимости от размера заполняемого блока. На нем по вертикальной оси прописаны размеры блока в байтах, гистограммы показывают скорость копирования в МБ/сек.



Не трудно видеть, что обычная функция существенно обгоняет SSE вариант на блоках до 1 мегабайта, однако проигрывает практически вдвое в случае, когда размер блока больше 1 мегабайта. Дополнительные измерения на отрезке от 500 000 байт до 1 000 000 байт показали, что смена лидера происходит в случае, когда размер блока примерно равен 590 000 байт. Не трудно догадаться, с чем связано такое поведение. У процессора, на котором производилось тестирование, L2 Cache – 512 КБ, соответственно, когда мы заполняли один и тот же блок постоянно, вся работа производилась внутри кеша, пока его хватало. На больших же блоках кеша не хватало, и мы имели резкое падение скорости работы обычной функции. Т.е. фактически мы имеем некорректный тестовый стенд.

Ну и что-то вроде вывода из всего этого хозяйства, который я сделал лично для себя. Если размер блока большой (больше мегабайта), то я пользую SSE вариант и не забиваю голову сомненьями. Если меньше, то смотрю по ситуации. Например, если много маленьких блоков разбросанных по памяти, и работать приходится то с одним то с другим, т.е. гарантировать, что все они поместятся в кешь и будут там постоянно находится я не могу, то снова пользую SSE вариант. А если надо 10 байт обнулить и потом с ними постоянно работать, то ну его этот SSE буду пользовать классический rep stosb.

А если серьезно, то вывод таков, что при оптимизации, надо не забывать учитывать наличие кеша процессора, и писать код с оглядкой на этот замечательный механизм. Будет время напишу более подробно, ибо вот memset то мы оптимизировали, а на подходе функция memcpy, которая так же крайне важна и нуждается в оптимизации.

четверг, 11 декабря 2008 г.

Про оптимизацию подробно

Решил собрать в кучу всякие мысли про оптимизацию. И расписать все это более или менее подробно.

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

Допустим, нам надо генерировать черно-белую картинку (более точно массив байт, где 0 - соответствует белому цвету, 255 - черному). Размеры картинки будем предполагать от 5000 до 10 000 пикселей по каждой из осей. Генерация будет заключаться в отрисовке заданного набора черных прямоугольников (от 500 до 1000 штук, с размерами от 100 до 200 пикселей по каждой из осей) на белом фоне. Так же ограничимся случаем, когда стороны прямоугольников параллельны осям.

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

В работе по оптимизации программы можно выделить три этапа или три класса оптимизации.

1. Архитектурная оптимизация
2. Алгоритмическая оптимизация
3. Оптимизация кода

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

Архитектурная оптимизация


Чаще всего дает максимальное увеличение скорости. Здесь оптимизация зачастую определяется не только конкретной задачей, но и контекстом, в котором эта задача решается (причем контекстом в первую очередь).

Обратимся к нашей модели. Нам надо понять: как будет использоваться сгенерированная картинка? Например, возможно картинка нужна нам для вывода на экран. Экраны размеров 5 000 на 5 000 маловероятны, и это позволяет нам при запросе создать только часть картинки. Даже, если по запросу вместо 25 000 000 = 5 000 * 5 000 пикселей, мы будем генерировать 1 920 * 1 080 = 2 073 600 пикселей (т.е. в 12 раз меньше), это даст существенный прирост в скорости генерации (надо понимать, что в зависимости от выбора алгоритма прирост может быть и не 12 кратный, а при особом упорстве можно добиться даже уменьшения скорости, нет вершин, которые бы не взял человеческий идиотизм).

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

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

Дабы не быть голословным рассмотрим еще несколько уточнений, которые могут существенно повлиять на выбор варианта решения.

Генерируется ли картинка разово (т.е. приняли набор прямоугольников, сгенерировали картинку, сбросили ее куда-нибудь на диск и забыли) или это итеративный процесс (запросили одно окно из картинки, потом слегка сдвинули окно - запросили, и т.д.). Если итеративный, могут ли данные меняться между запросами? Если данные могут меняться, то каким образом? Прямоугольники только добавляются или и удаляются тоже.

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

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


Алгоритмическая оптимизация


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

Вновь вернемся к примеру с закраской прямоугольников. Самый простой алгоритм, который можно предложить для ее решения такой: проходим по всем пикселям картинки, для каждого пикселя проверяем, лежит ли он внутри прямоугольника или нет, и соответственно выставляем в нужный байт 0xFF или 0x00. Алгоритм ужасен, поскольку в худшем случае (например, когда все прямоугольники совпадают и вырождены в точку), необходимо будет для каждого пикселя проверить все прямоугольники и не найти пересечения. Второй вариант, заполнить сначала нулями весь выделенный под изображение массив, а затем, перебирая прямоугольники, закрашивать покрываемые ими пиксели. На самом деле тоже не слишком хороший вариант, например, когда прямоугольников много и они равномерно покрывают изображения, начальное обнуление массива становится длительной и лишней операцией.

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

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

Оптимизация кода


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

На этом этапе кроме головы и компилятора, понадобятся еще и специфические инструменты - так называемые «профилировщики». Программ таких не сказать, что много, но их есть. Я пользуюсь CodeAnalyst-ом от AMD, он достаточно простой в работе, вполне устраивает меня по функциональности (хотя и имеет корявый интерфейс), а самое главное бесплатен, в отличие от схожей утилиты от Intel, стоящей что-то около 600 долларов (честно говоря, с Intel-овской утилитой не разбирался вполне допускаю, что там и возможностей побогаче и интерфейс поудобнее).

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

Именно на данном этапе некоторые из частей кода, возможно, необходимо будет переписать на ассемблере, а некоторые и оптимизировать, используя SIMD инструкции. Однако, здесь важно не переусердствовать, ибо современные компиляторы с оптимизацией кода справляются вполне успешно, и стоит соблюдать баланс между «читабельностью» (которой код на ассемблере обладает в минимальной степени) и скоростью работы.




На этом пока все. В данном случае отзывы и замечания по статье приветствуются.

четверг, 4 декабря 2008 г.

Несколько кусков кода

Хочу предложить некую подборку маленьких кусочков кода, для стандартных задач, часто встречающихся при программировании на MMX-SSE. Тут никаких откровений не будет, но может кому пригодится.

1. занулить все биты в SSE регистре (xmm0)

pxor  xmm0, xmm0


2. выставить все биты в SSE регистре (xmm0)

pcmpeqb  xmm0, xmm0


3. дублируем word CX в xmm0


movd xmm0, ecx ; xmm0 = ** ** ** ** | ** ** ** ** | ** ** ** ** | 00 00 VV UU
punpcklwd xmm0, xmm0 ; xmm0 = ** ** ** ** | ** ** ** ** | 00 00 00 00 | VV UU VV UU
pshufd xmm0, xmm0, 0 ; xmm0 = VV UU VV UU | VV UU VV UU | VV UU VV UU | VV UU VV UU


4. дублируем байт CL в xmm0



movd xmm0, ecx ; xmm0 = ** ** ** ** | ** ** ** ** | ** ** ** ** | 00 00 00 UU
punpcklwd xmm0, xmm0 ; xmm0 = ** ** ** ** | ** ** ** ** | 00 00 00 00 | 00 UU 00 UU
pshufd xmm0, xmm0, 0 ; xmm0 = 00 UU 00 UU | 00 UU 00 UU | 00 UU 00 UU | 00 UU 00 UU
movdqa xmm1, xmm0 ; xmm1 = 00 UU 00 UU | 00 UU 00 UU | 00 UU 00 UU | 00 UU 00 UU
psllw xmm1, 8 ; xmm1 = UU 00 UU 00 | UU 00 UU 00 | UU 00 UU 00 | UU 00 UU 00
por xmm0, xmm1 ; xmm0 = UU UU UU UU | UU UU UU UU | UU UU UU UU | UU UU UU UU


5. Перевернуть SSE регистр побайтово


__declspec( align(16) ) static BYTE g_carShufleMask[16] =
{
0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
};
_asm
{
movdqa xmm1, oword ptr [g_carShufleMask]
pshufb xmm0, xmm1
}


Последнее надо использовать аккуратно, pshufb - SSE2 команда (например, MSVC2003 вообще про нее не знает).

На последок. Крайне рекомендую найти и почитать статью "Using Block Prefetch for Optimized Memory Performance" очень хороший пошаговый разбор оптимизации функции memcpy с использованием кеша процессора. Скорость увеличивается в 2.5 раза (по статье в 3, но у меня на четырех компьютерах было от 2 до 2.5). Понятно, что ускорение memcpy (в особенности в тех условиях, которые заявлены в статье) не слишком интересно - но в целом почитать для начинающих вроде меня полезно.

понедельник, 3 ноября 2008 г.

Про оптимизацию и процессоры

Почитал тут статейку (старую году 2004-го) с сайта AMD про оптимизацию функции memcpy. Исходный вариант с использованием repb переделали на MMX с командой prefetch и прочей ерундой. По статье выходило ускорение в 3 раза, я попробовал получилось ускорение в 2 раза.

Но интересно другое. Я, конечно, слабо разбираюсь во всех этих процессорных заморочках, но вот есть команда repb, которая осуществляет копирование памяти. Производители процессоров прекрасно понимают, что копирование будет втрое быстрее за счет использования MMX. А почему сама команда repb не реализована в процессоре по тем же принципам, и с той же утроенной скоростью?

суббота, 30 августа 2008 г.

Про оптимизацию

Оптимизация дело полезное и нужное.

Продолжаю ковыряться в H264 декодере. Доковырялся до расскодирования ключевого фрейма, но получается медленно, решил пооптимизировать.

Итак, оптимизацию делал в двух направлениях, во-первых, перевел кой-какие куски кода на MMX/SSE, во-вторых, раскидал код по тредам для работы на нескольких ядрах.

Жизнь как обычно весела. Потратил неделю на перевод под MMX/SSE (ну правда чистого времени затратил часа три, если б еще кто-то дал мне три часа поработать только над этим, не отвлекая по разным проблемам), еще неделю пытался оптимизировать чтение из битового потока с арифметическим декодером и еще десять минут на многопроцессорность.

Так вот по результатам, ковырянием с битовым потоком уменьшил время декодирования на 5%, перевод под SSE уменьшил еще на 20%, а раскидывание по потокам в 2.5 раза. Что еще раз подтверждает тот факт, что прежде чем заниматься ковырянием мелочей надо попробовать максимально простые варианты.

воскресенье, 27 июля 2008 г.

Про MMX, SSE и оптимизацию

Вводная


Продолжаю делать H264 декодер. Надо соответственно сделать обратное преобразование блока 8х8 коэффициентов в 8х8 кусок плоскости (можно назвать это IDCT, но это будет не совсем правда, потому что там от косинусов осталось мало, а получилось что-то вроде Адамара, но не в этом суть). В спецификации H264 вся эта хрень осуществляется за 6 шагов (ну или за 7, как считать, может даже и за 8, для нашего текущего разговора это опять же не важно). Решил, что это хороший повод освежить свои навыки работы с ММХ и SSE.

Я уже как-то пытался попользовать эти мега расширения, но получилось достаточно смешно.

Дома у меня тогда стояла MSVS 2003 Standard. а Standard он очень хороший, только вот опции оптимизации компилятора в свойствах проекта он включить не дает (ну или я как обычно где-то, чего-то не нашел). Но я чего-то про это не особо задумывался. Соответственно, набросал кусок кода на С++, потом сделал код с той же функциональностью под SSE, сравнил скорость, и возрадовался. Разница была где-то раз в десять (естественно в пользу SSE). Правда радовался я до момента пока не скомпилил тот же тест на работе, где стояла MSVS 2003 Prof. Оказалось, что компилятор оптимизирует так же хорошо, как и я. Только у меня процесс оптимизации занимает два часа, а у него пару секунд. Но там задачка изначально, плохо ложилась на SSE, так что эксперимент был не слишком чистым.

Возвращаясь к H264. Здесь все очень красиво, преобразования с матрицей 8x8 (в матрице элементы типа short) плюс первые три шага используют только сложение вычитание строк этих матриц, как векторов. В общем, если не распробовать эти MMX с SSE здесь, то уж не понятно, где их и пробовать.

Формулировка задачи


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

Итак, на входе матрица
short arBufferSrc[8][8];

Мы эту матрицу преобразуем, результат помещаем в новую память (можно собственно и на месте крутить, но оно только усложняет код, а мне этого на данном этапе не хотелось бы). На С++ это выглядит так:
показать код
short arBufferSrc[64];
short arBufferCPPDst[64];
short *pBufferSrc = arBufferSrc;
short *pBufferCPPDst = arBufferCPPDst; memset(pBufferCPPDst, 0, 64);
for (int i=0;i<8;i++)
{
pBufferCPPDst[0 * 8 + i] = pBufferSrc[0 * 8 + i] + pBufferSrc[4 * 8 + i];
pBufferCPPDst[1 * 8 + i] = -pBufferSrc[3 * 8 + i] + pBufferSrc[5 * 8 + i]
-pBufferSrc[7 * 8 + i] - (pBufferSrc[7 * 8 + i]>>1);
pBufferCPPDst[2 * 8 + i] = pBufferSrc[0 * 8 + i] - pBufferSrc[4 * 8 + i];
pBufferCPPDst[3 * 8 + i] = pBufferSrc[1 * 8 + i] + pBufferSrc[7 * 8 + i]
-pBufferSrc[3 * 8 + i] - (pBufferSrc[3 * 8 + i]>>1);
pBufferCPPDst[4 * 8 + i] = (pBufferSrc[2 * 8 + i]>>1) - pBufferSrc[6 * 8 + i];
pBufferCPPDst[5 * 8 + i] = -pBufferSrc[1 * 8 + i] + pBufferSrc[7 * 8 + i] +
pBufferSrc[5 * 8 + i] + (pBufferSrc[5 * 8 + i]>>1);
pBufferCPPDst[6 * 8 + i] = pBufferSrc[2 * 8 + i] + (pBufferSrc[6 * 8 + i]>>1);
pBufferCPPDst[7 * 8 + i] = pBufferSrc[3 * 8 + i] + pBufferSrc[5 * 8 + i] +
pBufferSrc[1 * 8 + i] + (pBufferSrc[1 * 8 + i]>>1);
}

Все вроде понятно (кому не понятно, тот соответственно бросает читать, и идет пробовать себя в менеджменте, там, по слухам, еще и платят больше). Отметим один важный момент, на каждом шаге цикла мы используем данные только из i-го столбца, и результат так же будет помещаться в i-ом столбце.

Все работает, код нам потом пригодится проверять результаты работы MMX-шного и SSE-шного вариантов

Оптимизация с использованием MMX


Принцип оптимизации простой. Если в исходном коде мы работали с одним столбцом за раз, то, используя MMX регистры, мы можем обработать сразу 4 столбца. Выглядит это примерно так:
показать код
short arBufferSrc[64];
short arBufferMMXDst[64];

short *pBufferSrc = arBufferSrc;
short *pBufferMMXDst = arBufferMMXDst; memset(pBufferMMXDst, 0, 64);

__asm
{
mov eax, pBufferSrc
mov ecx, pBufferMMXDst
mov edx, 2
$loop:
movq mm0, qword ptr [eax]
movq mm1, qword ptr [eax + 4*16]
paddsw mm0, mm1
movq qword ptr [ecx], mm0

movq mm0, qword ptr [eax]
movq mm1, qword ptr [eax + 4*16]
psubsw mm0, mm1
movq qword ptr [ecx + 2*16], mm0

movq mm0, qword ptr [eax + 3*16]
movq mm1, qword ptr [eax + 5*16]
movq mm2, qword ptr [eax + 7*16]
psubsw mm1, mm0
psubsw mm1, mm2
psraw mm2, 1
psubsw mm1, mm2
movq qword ptr [ecx + 1*16], mm1

movq mm0, qword ptr [eax + 1*16]
movq mm1, qword ptr [eax + 3*16]
movq mm2, qword ptr [eax + 7*16]
paddsw mm0, mm2
psubsw mm0, mm1
psraw mm1, 1
psubsw mm0, mm1
movq qword ptr [ecx + 3*16], mm0

movq mm0, qword ptr [eax + 2*16]
movq mm1, qword ptr [eax + 6*16]
psraw mm0, 1
psubsw mm0, mm1
movq qword ptr [ecx + 4*16], mm0

movq mm0, qword ptr [eax + 1*16]
movq mm1, qword ptr [eax + 5*16]
movq mm2, qword ptr [eax + 7*16]
psubsw mm2, mm0
paddsw mm2, mm1
psraw mm1, 1
paddsw mm2, mm1
movq qword ptr [ecx + 5*16], mm2

movq mm0, qword ptr [eax + 2*16]
movq mm1, qword ptr [eax + 6*16]
psraw mm1, 1
paddsw mm0, mm1
movq qword ptr [ecx + 6*16], mm0

movq mm0, qword ptr [eax + 1*16]
movq mm1, qword ptr [eax + 3*16]
movq mm2, qword ptr [eax + 5*16]
paddsw mm1, mm2
paddsw mm1, mm0
psraw mm0, 1
paddsw mm1, mm0
movq qword ptr [ecx + 7*16], mm1

add eax, 8
add ecx, 8
dec edx
jne $loop
emms
}

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

Замеры скорости


Понятно, что отмерить время выполнения одного преобразования не представляется возможным, по причине и его крайней малости, поэтому, мы будем выполнять наши куски по 1 000 000 (одному миллиону) раз. Так же, чтобы никто нам не помешал, выставим побольше приоритет текущему процессу (SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_TIME_CRITICAL);) и не забудем вернуть его в нормальный по окончанию работы.

А как Вы будете непосредственно время измерять - вариантов, масса сами придумаете.

А я пока расскажу про свои итоги. Вариант номер раз, C++ код - время выполнения миллиона преобразований составляет 111 мс. Вариант номер два, оптимизация под MMX - 24 мс. Я считаю, что это очень достойный результат оптимизации.

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

Основное что нам надо запомнить на данном этапе, это то, что при использовании MMX скорость увеличилась в 4-5 раз. Перейдем теперь к SSE.

Оптимизация с использованием SSE


Принцип оптимизации тот же что и с MMX, только теперь мы будем обрабатывать все 8 столбцов за раз. В данном случае представленный код уже прооптимизирован (в меру моих скромных способностей), на предмет исключения повторной загрузки из памяти в регистры.
показать код
short arBufferSrc[64];
short arBufferSSEDst[64];

short *pBufferSrc = arBufferSrc;
short *pBufferSSEDst = arBufferMMXDst; memset(pBufferSSEDst, 0, 64);

__asm
{
mov eax, pBufferSrc
mov ecx, pBufferSSEDst

movdqa xmm0, [eax]
movdqa xmm2, xmm0
movdqa xmm1, [eax + 4*16]
paddsw xmm0, xmm1
movdqa [ecx], xmm0

psubsw xmm2, xmm
movdqa [ecx + 2*16], xmm2

movdqa xmm0, [eax + 2*16]
movdqa xmm2, xmm0
movdqa xmm1, [eax + 6*16]
psraw xmm0, 1
psubsw xmm0, xmm1
movdqa [ecx + 4*16], xmm0

psraw xmm1, 1
paddsw xmm1, xmm2
movdqa [ecx + 6*16], xmm1

movdqa xmm0, [eax + 1*16]
movdqa xmm1, [eax + 3*16]
movdqa xmm2, [eax + 5*16]
movdqa xmm3, [eax + 7*16]
movdqa xmm4, xmm0
movdqa xmm5, xmm1
movdqa xmm6, xmm2
movdqa xmm7, xmm3
psubsw xmm6, xmm1
psubsw xmm6, xmm3
psraw xmm7, 1
psubsw xmm6, xmm7
movdqa [ecx + 16], xmm6

paddsw xmm4, xmm3
psubsw xmm4, xmm5
psraw xmm5, 1
psubsw xmm4, xmm5
movdqa [ecx + 3*16], xmm4

movdqa xmm6, xmm2
psubsw xmm3, xmm0
paddsw xmm3, xmm6
psraw xmm6, 1
paddsw xmm3, xmm6
movdqa [ecx + 5*16], xmm3

paddsw xmm1, xmm2
paddsw xmm1, xmm0
psraw xmm0, 1
paddsw xmm1, xmm0
movdqa [ecx + 7*16], xmm1

emms
}

Меряем для этого кода время аналогично тому, как делали для MMX - получаем 19 мс. Ага. Прирост относительно MMX варианта, прооптимизированого для работы с памятью, 5%. Упс. Но, как выясняется, столь скромный результат связан с тем, что у меня AMD Athlon, на Intel-овском процессоре получилось ускорение на 50% (соответственно, для MMX получили 17 мс, для SSE - 9 мс). Выводы делать не буду, собственно, для меня понятно, что надо пользовать SSE, если возможно, потому что и 5% тоже хлеб, а 50% это уже и масло.

Надо сделать одно замечание по поводу SSE.

Для загрузки данных из памяти в регистры мы используем оператор movdqa, для него есть аналог movdqu, если использовать его, то вместо прироста скорости относительно MMX мы получим спад. Однако, чтобы использовать movdqa и не получить Run time error, исходные данные должны быть выровнены в памяти к 16 байтам. Это можно сделать либо автоматически:
__declspec(align(16) ) short arBufferSrc[64];

либо выделяя памяти побольше и подравнивая руками.

Заключение


Итак, MMX и SSE оптимизация не представляет сложностей, все работает на задаче, которая к этой оптимизации хорошо подходит. Относительно исходного C++ варианта использование SSE дает нам ускорение от 5 раз в худшем случае, до 10 раз в лучшем.