Лабораторная работа2
.pdfЛабораторная работа № 2. Векторная обработка данных с использованием SIMDкоманд
1 Цель работы
Изучение методов и получение навыков составления программ векторной обработки данных с использованием команд SSE.
2 Краткая теория
Большинство персональных ЭВМ до последнего времени являются обычными, последовательными компьютерами (SISD), в которых в каждый момент времени выполняется лишь одна операция над одним элементом данных (числовым или какимлибо другим значением).
SIMD (Single Instruction, Multiple Data, одиночный поток команд,
множественный поток данных, ОКМД) – принцип компьютерных вычислений, позволяющий обеспечить параллелизм на уровне данных. SIMDпроцессоры называются также векторными.
Настоящие SIMDкомпьютеры состоят из одного командного процессора (управляющего модуля), называемого контроллером, и нескольких модулей обработки данных, называемых процессорными элементами. Управляющий модуль принимает, анализирует и выполняет команды. Если в команде встречаются данные, контроллер рассылает на все процессорные элементы команду, и эта команда выполняется на нескольких или на всех процессорных элементах. Каждый процессорный элемент имеет свою собственную память для хранения данных. Одним из преимуществ данной архитектуры считается то, что в этом случае более эффективно реализована логика вычислений. До половины логических инструкций обычного процессора связано с управлением выполнением машинных команд, а остальная их часть относится к работе с внутренней памятью процессора и выполнению арифметических операций. В SIMD компьютере управление выполняется контроллером, а «арифметика» отдана процессорным элементам.
При кодировании и декодировании потоковых аудио и видео данных и изображений выполняется множество однотипных операций над различными, не связанными между собой частями. Эти команды можно было бы выполнять параллельно, однако архитектура МП x86 подразумевала, что за такт должна выполняться лишь одна инструкция
(SISD (Single Instruction, Single Data) или ОКОД (Одиночный поток Команд,
Одиночный поток Данных)). Для получения преимуществ SIMD в МП x86, начиная с процессоров Pentium, были введены наборы специальных команд, позволяющие обрабатывать за один такт несколько данных.
На данный момент существует целое семейство SIMDрасширений, используемые в процессорах архитектуры x86 (MMX, 3DNow!, SSE, SSE2, SSE3, SSSE3, SSE4).
Streaming SIMD Extensions (SSE) – набор команд процессора,
позволяющий обрабатывать за одну инструкцию сразу большой объём данных. В программировании используется в основном для обработки однотипных данных, вроде векторной математики, графики, звука и видео.
Ставшие стандартными SSE расширения в идеале могут ускорить тяжёлую математику в четыре раза – в одном SSE регистре хранится четыре float32 или int32 числа, операции над которыми проводятся одновременно.
Интринсик (intrinsic) – в компьютерной технике, это специальные функции, предназначенные для использования в данном языке и встроенные в компилятор. Зачастую они заменяются набором готовых инструкций, наподобие инлайнфункций(встроенные функций, которые не вызываются лишний раз, а импортируются из определенного места), однако в отличие от них компилятор может более качественно оптимизировать подставляемый код. Обычно используются как обертки над машинным кодом для оптимизации работы программы.
Существует два основных подхода к использованию SSE в своём приложении:
прописывать руками всю логику работы, используя расширения команд и ассемблерные вставки или так называемые интринсики;
доверить эту работу оптимизатору кода.
Как показывает практика, сгенерировать SSE код эффективнее, чем это сделает за вас оптимизатор, действительно непросто. Однако можно переписать узкое место вашей программы, используя эту технологию.
Существует два пути сделать это:
Использование ассемблерных вставок в коде.
floata[]={0.0f,1.0f,2.0f,3.0f}; floatb[]={4.0f,5.0f,6.0f,7.0f}; floatc[4];
_asm
{
//поместить четыре числа с плавающей точкой из массива aв регистр xmm0 movupsxmm0,a
//поместить четыре числа с плавающей точкой из массива bв регистр xmm0 movupsxmm1,b
addpsxmm1,xmm0//сложить четыре пары чисел movupsc,xmm1//поместить результат в массив c
};
Использовать так называемые интринсики (Данный способ, считается чуть более правильным).
Однако при не достаточно умелом подходе производительность может не только возрасти, но и упасть.
Мы будем рассматривать интринсики над SSE инструкциями, встроенными в компилятор в виде обычных C++ функций.
Наборы стандартных типов данных и интринсиков находятся в следующих заголовочных файлах:
xmmintrin.h – описания MMX и SSE интринсиков. emmintrin.h – описания SSE2 интринсиков.
Перечень основных интринсиков и типов данных представлен в приложении А.
Интринсики используются примерно также как и ассемблерные вставки, только с использованием синтаксиса обычного языка. Очень важная вещь, которую нужно обязательно помнить – это то, что нужно использовать выровненные в памяти по границе 16 байт данные.
Рассмотрим следующий пример. Предположим необходимо вычислить корень для каждого элемента массива. Псевдокод для решения данной задачи представлен ниже:
foreachfinarray f=sqrt(f)
или если уточнить:
foreachfinarray
{
загрузить fв регистр с плавающей точкой; вычислить корень; сохранить результат обратно в памаять;
}
Процессор, поддерживающий SSE имеет восемь 128битных регистров, каждый из которых может содержать 4 16битных (float) числа с плавающей точкой. SSE представляет собой набор инструкций которые позволяют загружать числа с плавающей точкой в 128битные регистры, выполнять арифметические и логические операции над ними и сохранять результат обратно в память.
При использовании технологии SSE, алгоритм может быть переписан следующим образом:
foreachfinarray
{
загрузить 4элемента в регистр SSE; вычислить 4корня за одну операцию; сохранить результат обратно в память;
}
Программисты на C++ могут писать программы используя SSE интринсики не заботясь о регистрах. У них есть 128байтный тип __m128и набор функций для выполнения арифметических и логических операций. А компилятор C++ решает какой из SSE регистров использовать для оптимизации кода. Технология SSE может быть использована тогда, когда
некоторые операции выполняются над каждым элементом длинного массива.
Все инструкции SSE и тип данных __m128 определены в файле xmmintrin.h. Так как SSE инструкции являются предопределенными для компилятора, то нет необходимости подключать libфайлы.
Каждый массив float, обрабатываемый инструкциями SSE должен быть выровнен по границе 16 байт (то есть каждый элемент массива должен занимать равно 16 байт). Статический массив должен объявляться с
ключевым словом __declspec(align(16)):
__declspec(align(16))floatm_fArray[ARRAY_SIZE];
Динамические массивы должны использовать функцию
_aligned_mallocи уничтожаться потом при помощи _aligned_free.
m_fArray=(float*)_aligned_malloc(ARRAY_SIZE*sizeof(float),16);
...
_aligned_free(m_fArray);
((float*)-приведение к типу “указатель на переменную типа float”.Подробнее смотри по теме “Указатели в Си”)
Переменные типа __m128 используются в качестве операндов инструкций SSE. К ним нельзя обращаться напрямую. Переменные типа _m128автоматически выравниваются по 16байтной границе.
Вот примеры инструкций SSE для компилятора Visual C++:
Сложение: _mm_add_ps(__m128 A, __m128 B) Умножение: _mm_mul_ps(__m128 A, __m128 B) Извлечение корня: _mm_sqrt_ps(__m128 A, __m128 B) Умножение: _mm_mul_ps(__m128 A , __m128 B ) Деление: _mm_div_ps(__m128 A , __m128 B )
Эти функции принимают параметры типа __m128. Чтобы использовать определенные числа в этих функциях, нужно их сначала записать в переменную типа __m128:
__m128m3_25=_mm_set_ps1(3.25f);//создаем переменную m3_25,которая будет хранить четыре значения.Заполняем все значения числом 3.25.
__m128m0_76=_mm_set_ps1(0.76f);//сохраняем 0.76в другую переменную
__m128m1=_mm_mul_ps(m3_25,m0_76);//используем заданные в коде числа.
Инструкции SSE могут быть использованы только если они поддерживаются процессором. В приложении Б приведен пример кода, позволяющий определить поддержку SSE, MMX и других возможностей микропроцессора.
Ниже приведен пример программы, демонстрирующей работы с SSE и результаты ее работы.
Пример 2.1 Пример работы с расширением SSE
//SSEIntrinsics.cpp:Definestheentrypointfortheconsoleapplication. #include<cstdio>
#include<cmath> #include<ctime> #include<malloc.h> #include<xmmintrin.h>
//Вычисляем функцию через cpp:y=sqrt(x+0.5)
voidComputeArrayCpp( |
//[in]исходный массив |
float*pArray, |
|
float*pResult, |
//[out]массив результатов |
intnSize) |
//[in]размер массивов |
{ |
|
inti; |
|
float*pSource=pArray; |
|
float*pDest=pResult; |
|
for(i=0;i<nSize;i++)
{
*pDest=(float)sqrt((*pSource)+0.5f);
//Перемещаемся к следующему элементу pSource++;
pDest++;
}
}
//Вычисляем функцию через sse:y=sqrt(x+0.5) voidComputeArrayCppSSE(
float*pArray, |
//[in]исходный массив |
float*pResult, |
//[out]массив результатов |
intnSize) |
//[in]размер массивов |
{ |
|
//Определяем кол-во итераций цикла: |
|
//sizeof(__m128)/sizeof(float)=>4 |
|
intnLoop=nSize/4; |
|
__m128m1; |
|
//Преобразуем указатели на floatк типу __m128 __m128*pSrc=(__m128*)pArray;
__m128*pDest=(__m128*)pResult;
__m128m0_5=_mm_set_ps1(0.5f); |
//m0_5[0,1,2,3]=0.5 |
for(inti=0;i<nLoop;i++) |
|
{ |
|
m1=_mm_add_ps(*pSrc,m0_5); |
//m1=*pSrc+0.5 |
*pDest=_mm_sqrt_ps(m1); |
//*pDest=sqrt(m1) |
//Перемещаемся к следующему элементу __m128 //Тем самым пропускаем 4floatиз массива pSrc++;
pDest++;
}
}
//Инициализируем массив начальными занчениями sin(rnd()) voidinit(float*a,intsize)
{
for(inti=0;i<size;i++)
{
floatx=(float)rand()/RAND_MAX; a[i]=sin(x);
}
}
int_tmain(intargc,_TCHAR*argv[])
{
//Используем русскую локаль setlocale(LC_ALL,"Russian");
constintMAX_SIZE=10000000;
//Тем самым пропускаем 4floatиз массива
float*x=(float*)_aligned_malloc(sizeof(float)*MAX_SIZE,16); float*y=(float*)_aligned_malloc(sizeof(float)*MAX_SIZE,16);
DWORDstartTime,endTime;
//Получаем время начала работы startTime=GetTickCount();
init(x,MAX_SIZE);
//Получаем время окончания работы endTime=GetTickCount();
printf("Инициализация массивов:%dмс\n",endTime-startTime);
startTime=GetTickCount();
//Выполняем вычисления с помощью обычных функций c++ ComputeArrayCpp(x,y,MAX_SIZE);
endTime=GetTickCount();
printf("Вычисление средствами C++:%dмс\n",endTime-startTime);
startTime=GetTickCount();
//Выполняем вычисления с помощью SSE
ComputeArrayCppSSE(x,y,MAX_SIZE);
endTime=GetTickCount();
printf("Вычисление средствами SSE:%dмс\n",endTime-startTime);
//Тем самым пропускаем 4floatиз массива
_aligned_free(x); _aligned_free(y);
return0;
}
Результат работы программы:
Инициализация массивов:2855мс Вычисление средствами C++:889мс Вычисление средствами SSE:62мс
3Задание
1)Составить программу применяющую функцию к входным массивам с получением на выходе массива вычисленных значений с применением инструкций векторной обработки данных SSE.
2)Сравнить время вычисления функции и общее время работы с результатами без использования SSE.
3)Оценить ускорение общего времени работы программы и непосредственно вычислений.
4)Произвести эксперименты на массивах размером в 10 000 000, 50 000 000 и 100 000 000 элементов.
5)Оценить объем памяти необходимый для размещения этих элементов в памяти.
6)Результаты представить в виде таблицы представленной ниже.
Показатель |
Колво |
|
|
элементов |
|
|
|
|
|
|
|
|
10 000 000 |
50 000 000 |
100 000 000 |
Объем занимаемой памяти, Кб |
|
|
|
Время вычисления без SSE, мс |
|
|
|
Время вычисления без SSE, мс |
|
|
|
Ускорение вычислений |
|
|
|
Общее время работы без SSE, мс |
|
|
|
Общее время работы c SSE, мс |
|
|
|
Общее ускорение |
|
|
|
Рисунок 2.1 –Таблица для представления отчета
4 Варианты заданий
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
* Во всех вышеперечисленных функциях .