Uploaded by Магомед Мержоев

III semestr

advertisement
Введение в АССЕМБЛЕР ............................................................................................................................ 2
1. Регистры ............................................................................................................................. 2
2. Адресация ........................................................................................................................... 3
Оптимизация. Уровни оптимизации........................................................................................................... 6
1) Оптимизации алгоритма - переход от одного алгоритма к другому. ............................. 6
3) Машинно-независимая оптимизация циклов ..................................................................... 7
4.) Оптимизация переходов .................................................................................................... 12
Ошибки и обратный эффект оптимизаций ............................................................................... 14
5.) Машинно-зависимая оптимизация ................................................................................... 15
6.) Оптимизации, выполняемые компиляторами ................................................................. 19
7.) Оптимизации ...................................................................................................................... 20
Приемы оптимизаций ................................................................................................................................. 23
Жизненный цикл программ ....................................................................................................................... 28
Этапы: ....................................................................................................................................... 28
Типовой процесс разработки ПО по ГОСТу......................................................................... 29
Модели ЖЦП ........................................................................................................................... 30
Тестирование и отладка ............................................................................................................................. 35
Невозможность исчерпывающего тестирования ...................................................................... 35
Тестирование модулей ................................................................................................................ 41
Универсальные вычисления ...................................................................................................................... 44
на графических процессорных устройствах GPGPU .............................................................................. 44
CUDA ............................................................................................................................................ 44
Оптимизация программ на CUDA ......................................................................................... 52
OpenCL (Open Computing Language) ......................................................................................... 55
Сравним алгоритм суммирования двух массивов на CUDA и OpenCL................................. 56
На CUDA: ................................................................................................................................. 56
На OpenCl: ................................................................................................................................ 57
1
Введение в АССЕМБЛЕР
1. Регистры
Регистр – это максимально быстрая память для временного хранения каких-либо
данных.
EAX (32-ух битный регистр)
ax (16 бит)
ah (8 бит)
al (8 бит)
eax
ebx
ecx
edx
EBX
bx (16 бит)
bh (8 бит) bl (8 бит)
cx
ch
dx
cl
dh
dl
регистры общего назначения
Регистры si (индекс источника) и di (индекс приемника) используются в сроковых
операциях. Регистры pb и sp задействуются при работе со стеком.
si
индекс источника
di
индекс приемника
pb
sp
регистры для работы со стеком
2
2. Адресация
Адресация бывает двух видов:
 прямая (непосредственная) это простейший вид адресации операнда в памяти, так как эффективный адрес
содержится в самой команде и для его формирования не используется никаких
дополнительных источников или регистров. Эффективный адрес берется
непосредственно из поля смещения машинной команды (см. рис. 1), которое может
иметь размер 8, 16, 32 бит. Это значение однозначно определяет байт, слово или
двойное слово, расположенные в сегменте данных.
move eax,0

// обнулили регистр eax
косвенная – записывается в квадратных скобках [ ]
равнозначно «взять значение по указателю» в Си При такой адресации эффективный адрес операнда может находиться в любом из
регистров общего назначения, кроме sp/esp и bp/ebp (это специфические регистры для
работы с сегментом стека). Синтаксически в команде этот режим адресации
выражается заключением имени регистра в квадратные скобки [ ]. К примеру,
команда mov ax,[ecx] помещает в регистр ax содержимое слова по адресу из сегмента
данных со смещением, хранящимся в регистре ecx. Так как содержимое регистра
легко изменить в ходе работы программы, данный способ адресации позволяет
динамически назначить адрес операнда для некоторой машинной команды. Это
свойство очень полезно, например, для организации циклических вычислений и для
работы с различными структурами данных типа таблиц или массивов.
[eax] // «взять» содержимое ячейки памяти, адрес кот. находится в регистре eax
[eax + 1] // взять число из ячейки памяти eax + 1, то есть ячейки массива A[1]
[eax + ebx] ≡ A[ i]
i-ый элемент
начальный
элемент
[eax + ebx*4] ≡ int A[ i]
Тип возвращаемого значения можно указать:
bute prt [ ] – взять байт по указанному адресу
word prt [ ] – взять слово (16 бит) по адресу
dword prt [ ] – взять 32 бита
qword prt [ ] – взять 64 бита
bute prt [ ] ≡ *((char*)eax)
на Си
3
3. Система команд
1) Команда пересылки mov
Move operand to/from system registers - Пересылка операнда в/из системных регистров.
Иными словами, команда выполняет присвоение.
Примеры:
mov eax,ebx
mov eax, 0
mov eax, dword prt[edi]
mov bh, [eax]
mov [esi + 100], edx
// регистр eax запишет значение регистра ebx
// запись числа в регистр (прямая адресация)
// пересылаем в регистр число по данному адресу
// пересылаем 1 байт (т.к. bh) из eax в bh
//операция «Записть в память»
Два операнда, содержащиеся в команде mov должны иметь одинаковую длину. Если первый
операнд - регистр общего назначения, то второй операнд может быть любым. Если же в
качестве операнда используется сегментный регистр, то использование второго сегментного
регистра запрещено, также, запрещено использование двух адресов памяти.
2) Арифметические операции
`+` add Addition - команда сложения
`-` sub Subtract - команда вычитания
`*` imul Integer Multiply - знаковое целочисленное умножение
`/ ` idiv Integer Divide - знаковое целочисленное деление
`++` inc Increment - команда увеличивает значение операнда на 1
`- -` dec Decrement - команда уменьшает значение операнда на 1
3) Логический операции
 or op1, op2
Операция логического сложения. Команда выполняет поразрядно логическую операцию
ИЛИ (дизъюнкцию) над битами операндов op1 и op2. Результат записывается на место op1.
 and op1, op2
Oперация логического умножения. Команда выполняет поразрядно логическую
операцию И (конъюнкцию) над битами операндов op1 и op2. Результат записывается на
место op1.
 xor op1, op2
Операция логического исключающего сложения. Команда выполняет поразрядно
логическую операцию исключающего ИЛИ над битами операндов op1 и op2. Результат
записывается на место op1.
 not op1
Операция логического отрицания. Команда выполняет поразрядное инвертирование
(замену значения на обратное) каждого бита операнда.
4) Команды передачи управления
Безусловная передача управления jmp // как go to в Си
jmp передает управление в другую точку программы, не сохраняя какой-либо информации
для возврата. Операндом может быть непосредственный адрес для перехода (в программах
используют имя метки, установленной перед командой, на которую выполняется переход), а
также регистр или переменная, содержащая адрес.
4. Флаги
4
Флаги – это специальный регистр, где с помощью битов выставляется состояние текущих
процессов 0 или 1.
Они автоматически устанавливаются после арифметических операций или порераций
сравнения.
Флаг z (zero) устанавливается, если операция привела к нулю
jz/ jnz jmp zero // перейти, если ноль/не ноль
Флаг с (carry)
// если после операции произошло переполнение
Флаг s (sign)
//отражает состояние старшего бита результата
jg/ jng
// переход, если больше
jl/ jnl //переход, если меньше
ja/ jna
// переход, если выше
jb/ jnb
// переход, если ниже
jge
jle
jae
jbe
// больше, либо рано
// меньше, либо рано
// выше, либо рано
// ниже, либо равно
При вызове процедуры:
push x
push y
call A
pop edx
pop edx
загрузка параметров
вызов процедуры
очистка стека
Пример: (перевод программы с языка Си на Ассемблер)
int A[10], B[10];
int i;
for (i = 0; i<=10; i++)
A[i] += B[i];
Пусть А=eax
B=ebx
i=ecx
xor ecx, ecx
loop:
mov edx, [eax+ecx*4]
add edx, [ebx+ecx*4]
mov [eax+ecx*4], edx
inc ecx
cmp ecx, 10
jl loop
5
Оптимизация. Уровни оптимизации.
1.)
2.)
3.)
4.)
5.)
Алгоритм
Машинно-независимые оптимизации
Машинно-зависимые оптимизации
Оптимизации на уровне ассемблера
Оптимизации внутри процессора
Оптимизация
По времени
По памяти
1) Оптимизации алгоритма - переход от одного алгоритма к другому.
Основные идеи:
 прейти к более простому алгоритму
 замена времени на память – замена вычислений на предвычисления (доступ к памяти)
for (j = 1; j<1000; j++)
{
A[j] = days – B[i]*j;
if ( A[j] < 0)
break;
}
R[i] = B[i] + A[j];
Нужно:
o вместо массива взять переменную
o days < B[i]*j
o всё сводится к оператору R[i] = days % B[i]
2) Машинно-независимая оптимизация операций
Таблица времени выполнения операций по
убыванию:
=
++, -+, -, [ ], <<, ^, &, |, <=, .>=, <, >
*
(int) // преобразование типа
pow
sqrt
sin, cos
Избавление от деления: a/b/с = a/(b*c)
Избавление от умножения: a+x*c+y*c = a+(x+y)*c
a*16 = a<<4
a/16 = a>>4
6
a %16 = a&(16-1)
a^5 = ((a*a)^2*a)
Преобразование типа:
float f;
int i;
i= 5+f+i; // (int)((float)5+f+(float)i)
можно заменить три преобразования одним: i = 5 + (int)f + i;
3) Машинно-независимая оптимизация циклов
I. Отказ от циклов (применяется при малом кол-ве прохождений по циклу)
Исходный код:
for (i =0; i<3; i++)
A[i] = 0;
Оптимизированный код:
Вариант №1:
A[0] = A[1] = A[2] = 0;
Вариант №2:
memset (A, 0, sizeof(A[0])*N;
II. Правильное вложение циклов
Исходный код:
for (i =0; i<1000; i++)
for (j =0; j<10; j++)
for (k =0; k<3; k++)
Оптимизированный код:
for (k =0; k<3; k++)
for (j =0; j<10; j++)
for (i =0; i<1000; i++)
A[i][j][k] = 0;
A[i][j][k] = 0;
III. Замена переменной цикла
Исходный код:
for (i =0; i<10; i++)
A[3*i+4]+= c;
Оптимизированный код:
j = 3*i +4;
for(j = 4; j<34; j+=3)
A[j]+= c;
7
IV. Вынесение инварианта из цикла
Исходный код:
for (i =0; i<10; i++)
for (j =0; j<10; j++)
Оптимизированный код:
for (i =0; i<10; i++)
{
A[i][j] = B[i][j] + d/i + d/k;
didk = d/i +dk;
Ai = A[i];
Bi = B[i];
for (j =0; j<10; j++)
A[i][j] = B[i][j] + didk;
}
В данном примере мы:
 заменили d/k = dk,
 вынесли d/I из второго цикла,
 вынесли dk
V. Развертка циклов – самая важная и эффективная из оптимизаций циклов (циклы могут
быть развернуты до 16 раз)
Исходный код:
for (i =0; i<N; i++)
A[i]= B[i]*C[i];
Оптимизированный код:
for (i =0; i<N; i+=2)
{
A[i]= B[i]*C[i];
A[i+1]= B[i+1]*C[i+1];
}
Преимущества:
 шаг увеличиваем вдвое => в два раза больше сравнений
 механически увеличили тело цикла в два раза => получили длинный линейный
участок кода, значит компилятор сможет оптимально распределить регистры
VI. Объединение циклов
Исходный код:
for (i =0; i<100; i++)
A[i]= B[i]*C[i] + 4;
for (j =0; j<100; j++)
Оптимизированный код:
for (i =0; i<100; i++)
{
A[i]= B[i]*C[i] + 4;
8
B[j] = E[j] +5;
B[j] = E[j] +5;
}
HO!! Массивы должны быть независимыми и не должны пересекаться.
9
VII. Разъединение циклов
Исходный код:
Оптимизированный код:
while (k>0)
if (x == y)
{
{
if (x == y)
while (k>0)
k −−;
k −−;
else
else
k −= x;
while (k>0)
}
k −= x;
}
VII. Исключение общих операций
∑ (x*A[i]) = x* ∑ A[i];
∑ (A[i] – k) = ∑ A[i] – k*n;
Исходный код:
Оптимизированный код:
for (i =0; i<10; i++)
t = A[0]*c;
{
for (i =0; i<10; i++)
B[i] = A[i]*c + q;
{
D[i] = A[i +1]*c;
B[i] = t+q;
}
D[i] = t + A[i +1]*c;
}
VIII. Избавление от ненужной индексации
Исходный код:
for (i =0; i<N; i++)
Оптимизированный код:
for (i =0; i<N; i++)
for (j =0; j<M; j++)
for (j =0; j<M; j++)
{
{
A[i][j] = 0;
temp = 0;
for( k = 0; k<K; k++)
for (k = 0; k<K; k++)
A[i][j] = B[i][k]*C[k][j];
temp+= B[i][k]*C[k][j];
}
A[i][j] = temp;
}
10
11
4.) Оптимизация переходов
Исходный код:
if (a==b)
Оптимизированный код:
if (a == b)
k = 0;
k = 0;
if (a!=b)
else
k = 1;
k = 1;
k = (a==b)?0:1;
k = (a!=b)
!!(a – b)


В списке вложенных условий if else ставить условия с учетом их вероятностей
Правило Деморгана (если стоит отрицание меняем всё на противоположное)
if(!(a>b)&&(x==y)
(a<b)||(x!=y)
 Удаление лишнего кода
Лишний код влияет только на размер программы, на время выполнения не влияет!
if( x*x <0)
{
……
}

// данное условие никогда не будет выполнено
Лишние присваивания
Исходный код:
Оптимизированный код:
t1 = b*b;
t2 = 4*a*c;
D = sqrt (b*b - 4*a*c);
t3 = t1 – t2;
D = sqrt (t3);

Повторные присваивания
x = 0;
y = 5;
x = y = 5;
x = y;
12
13
Ошибки и обратный эффект оптимизаций

Ошибки:
Исходный код:
for (i =0; i <n; i++)
Оптимизированный код:
if (y!=0)
{
xy = x/y;
if (y!=0)
for (i =0; i <n; i++)
A[i] += x/y;
A[i] += xy;
}

Ухудшение алгоритма, пример:
while (k>0)
{
if (y<0,0001)
// условие выполняется редко, а скорее всего вообще не будет
выполнено
s+= sin(x);
y*= (k--);
}
Если вынести sin(x), как инвариант, то один раз присваивание будет выполнено.

Результат чрезмерной развертки циклов:
Получим очень долгое выполнение, т.к. получится слишком много кода => всё может
не поместиться в КЭШ.
14
5.) Машинно-зависимая оптимизация
I. Размер машинного слова
A B C D
 Прямой порядок байт в слове (big endian)
ABCD
 Обратный порядок байт в слове (little endian) DCBA
 VAX — аббревиатура от «Virtual Address eXtension» BADC
(Часто применяется в криптографии – если данные хранятся в одном порядке, то делаем
XOR)
void encrupt (bute *buf)
# ifdef BIG_ENDIAN
/* побитово сдвигаем число*/
A = (uint) buf[0]
(uint) buf[1] << 8;
(uint) buf[2] << 16;
(uint) buf[3] << 32;
#else
A = *(uint*) &buf[0];
A^ = key[0];
II. Размеры типов данных
int x;
A = x <<16;
Получим A = 0, т.к. int<=16 байт
III. Размер страницы КЭШ
Пусть размер КЭШ 1Кб
int A[N][M]
for (i =0; i<256; i++)
for (j =0; j<16; j++)
A[j][i] = 0;
// считываем всю страницу, а удаляем один элемент =>
надо поменять индексацию A[i][j] = 0;
IV. Знание количества регистров
registr int i, j, k;
15
Сделали переменные циклов регистровыми. Если все переменные делать геристровыми, то
места в регистре может не хватить => регистровыми можно делать столько переменных,
сколько регистров свободно, а т.к.мы этой информации не знаем, то это метод лучше вообще
не применять.
16
V. Размеры системных буферов
a) Оверлей – специальный буфер, в котором храниться код программы => код
загружается в оперативную память не целиком, а частями.
Пусть у нас есть 11 процедур по 1Кб (в массиве f хранится 11 указателей на функцию)
Размер оверлея 10 Кб
while ()
{
for (i = 0; i<11; i++)
(*f)[i]();
}
В результате работы программы загружаем по одному указатели на функции. Записали 10
указателей. 11-ый не влезает, а значит он заменит самый давно используемый, т.е.1
Получится, что 11 заменит 1; 1 заменит 2; 2 на 3 и т.д.
В итоге мы каждый раз удаляем ту процедуру, которую будем вызывать.
=> нужно объявить оверлей 11Кб.
b) Размер буфера ввода/вывода
(например функции, начинающиеся в «f»: fwrite, fscanf
В Си стандартная функция, позворяющая увеличить размер буфера:
int setvbuf(
FILE *stream,
// указатель на структуру FILE
char *buffer,
// указатель на буфер, созданный пользователем
int mode,
size_t size
// режим буферизации
// размер буфера в байтах
);
VI. Ассемблерные вставки
asm
{
mov eax,5;
.....
}
VII. Замена индекса на указатель
(применима к циклам – уменьшаем количество операций в цикле, но не относится к
машинно-независимым)
Исходный код:
Оптимизированный код:
17
for (i =0; i<10; i++)
A[i] = B[i] + 5;
pA = &A[0];
pB = &B[0];
for ( ; pA<&A[10];pA++, pB++)
*pA =*pB+5;
18
6.) Оптимизации, выполняемые компиляторами
I. Исключение лишних загрузок в регистр
a=x+y
mov eax, y
(1)
mov ebx, x
(2)
add eax, ebx
mov a, eax
b = a+5
move eax, a
// лишняя строчка
add eax, 5
mov b, eax
II. Сортировка инструкций для их параллельного использования
Пользователю неважно, в каком порядке располагаются строчки (1) и (2) из п.I, но для
распределения процессорного времени, это может иметь значение.
Пример: mov eax, 0 то же самое, что и xor eax, eax
но лучше использовать второй вариант команды.
III. Замена вызовов функций её кодом
Получаем линейный участок кода. Не вызываем функцию, а просто подставляем её в код.
Пример: «inline» - спец. функция в Си
IV. Выравнивание команд и данных
Когда обращаемся к типу int, то доступ будет быстрее, если он будет размещен по адресу
кратному 4. В ассемблере существует операция nop, для того чтобы метки были
определенной кратности.
V. Замена простых циклов на циклические инструкции ассемблера
Существует специальный префикс loop.
Пример: функции memst, memcpy и др. могут быть заменены на циклические инструкции
процессора.
VI. Параллелизация (векторизация) кода
Исходный код:
for (i =0; i<10; i++)
A[i] = 0;
Оптимизированный код:
for (i =0; i<10; i+=4)
{
A[i] = 0;
A[i + 1] = 0;
19
A[i + 2] = 0;
A[i + 3] = 0;
}
VII . Оптимизация вызовов
 Стандартное соглашение о вызовах (calling convention)
Пусть существует функция:
int A (int, int, int);
int x, a, b, c;
x = A(a,b,c);

Соглашение для вызова функций «cdecl»
В данном случае аргументы передаются через стек и записываются с конца.
push с
push b
push a
call A
add esp, 12
mov x, eax
Свойства:
1.) Параметры передаются через стек справа налево.
2.) Вызывающая сторона после вызова функции обязана скорректировать стек
(хорошо, что за корректировку отвечает отправляющая сторона, т.к. только она
знает количество аргументов).
3.) Результат функции возвращается обратно через регистр eax.
4.) Регистры eax, eсx, edx доступны для использования функцией.

Соглашение fast call
Передаем первые два аргумента слева направо через регистры eсx, edx, оставшиеся
аргументы передаются через стек справа налево.
Недостаток: все функции должныs быть fastcall.

Соглашение о вызовах для 64 битных систем
Регистры rcx, rdx, r8, r9 используются для передачи целых чисел и указателей слева
направо. А регистры XMM0, XMM1, XMM2, XMM3 – для передачи параметро с
плавающей точкой.
Все остальные регистры передаются через стек. Результат попадает в регистр rax,
если целое и в XMM0, если плавающее.
7.) Оптимизации на уровне ассемблера
20
Вот, как выглядела раньше схема работы процессора:
Блок выборки и декодирования
Арифметако-логичское
устройство (АЛУ)
Память
Что появилось в процессорах для улучшения быстродействия:
1.) Многоуровневый КЭШ
2.) Параллельность
3.) Предвыборка инструкций и предсказание переходов
4.) Появление новых инструкций (MMX, SSE, AVX)
Схема современного процессора Intel:
Предсказание переходов
КЭШ инструкции
Блок
декодировани
я
Переименование и
размещение
регистров
Буфер
перекодирования
Отставка
Планировщик
Перевыборка
ALU
ALU
ALU
L2 - КЭШ
Память
Общая организация современных микропроцессоров.
Большинство современных микропроцессоров относятся к классу конвейерных, скалярных с
внеочередным выполнением операций.
Конвейерность – действия разбиваются на этапы с небольшим временем исполнения.
21
Суперсколярность – на каждом этапе обрабатывается сразу несколько потоков операций
одновременно.
Внеочередное исполнение – операции не обязаны выполняться строго в том порядке, в
котором они идут в программном коде.
22
Приемы оптимизаций
1.) Предсказание переходов (нужно попытаться отказаться от переходов)
 x = (a < b)?const1: const2;
Исходный код
cmp a, b
jge l1
Оптимизированный код
xor eax, eax
// непредсказуемый переход
cmp a, b
mov eax, const1
setge bl
jmp l2
sub ebx, 1
mov ebx, const2
and ebx, const1 – const2
add ebx, const2

y = |x|
cdq eax
xor eax, edx
sub eax, edx
// бит знака данного регистра распространяется на весь регистр
или:
test eax, eax
cmoveq edx, ecx

// если не равны, то записываем ecx в edx, если равны – ничего не
происходит
Использование статического предсказания переходов
(когда переход осуществляется вверх, процессор считает, что это цикл, следовательно
он мнится, как происходящий; если вниз, то процессор считает это выходом из цикла,
следовательно, скорее всего он не происходит)

Развертка циклов

inline подстановки функций сокращают число переходов
2.) Оптимизация доступа к памяти
 Выравнивать данные и начало цикла.
 Если необходимо получить доступ к невыровненным данным, то лучше сначала
использовать выровненные, а потом, с помощью сдвигов, уже к тем данным, которые
были нужны изначально.
 Избегать короткого чтения после длинной записи.
 Избегать длинного чтения после короткой записи.
mov [ebx + 10], bl
mov [ebp + 10], bl
mov eax, [ebp + 10]
movz eax, bl
// расширили регистр,
дополнив его нулями

Не использовать самомодифицирующийся код, т.к. нет предсказания и предвыборки
инструкций.
23
3.) Правильный выбор инструкций

Не использовать сложных, длинных инструкций, например: enter, leave, loop, xlat


Использовать команду lea
Заменить команды
inc
на
add
dec
на
sub


Использовать xor для обнуления регистров
Вместо сдвига использовать сложение

shl
на add
Заменить команду умножения

imul eax, 5
Деление
на
lea eax, [eax*4 + eax]
Пример: хотим найти q = x/d, где d – целая константа
b = (количество значимых бит в d) – 1
r = (размер машинного слова) + b
f = (2^r) / d
Случай 1: Если , то q =x>>b
Случай 2: Если дробная часть f < 0.5, то q = ((x+1)*[f]) >> b
Случай 3: Если дробная часть f > 0.5, то q = (x*]f[) >> b
4.) Использование SIMD инструкций (векторизация)
Появились новые регистры и типы данных:
MMX – 64 бит
XMM – 128 бит
YMM – 256 бит
Рассмотрим 128 битный регистр.
В нем 16 полей по 8 бит
(16*8) bute
или
8 полей по 16 бит
(8*16) word
4 по 32
(4*32) dword
2 по 64
(2*64) qword
Общий вид выражения:
[p][команда][s/u][b/w/d/q/s]
целое
signed /
unsigned
Пример1:
uchar A[256]
uint A[256];
for (i =0; i<256; i++)
s = 0x03020100;
A[i] = i;
for (i =0; I < (256/4); i++)
{
A[i] = s;
24
s+=0x04040404;
}
25
Сделаем то же самое с использованием SSE регистра:
loop:
mov
ecx,
64
movdqa
xmm0, 0706050403020100h
mov
xmm1, 0808080808080808
movdqa
[eax],
paddd
xmm0, xmm1
add
eax,
dec
ecx
jne
loop
xmm0
8
Можно так же развернуть цикл и писать не один элемент, а два:
mov
ecx,
32
movdqa
xmm0, 0706050403020100h
mov
xmm1, 0808080808080808
movdqa
[eax],
paddd
xmm0, xmm1
movdqa
[eax + 8], xmm0
paddd
xmm0, xmm1
dec
ecx
xmm0
jne
Пример2 (реализация функции strlen(s)):
mov
ecx,
eax
pxor
xmm0, xmm0
and
ecx,
15
and
eax,
-16
movdqa
xmm1, [eax]
Находим адрес, который кратен 16
// считываем всё, что входит в 16 байт
pcmpeqb xmm1, xmm0
//сравнить побайтово на равенство целые числа
pmovmskb
// копирует один старший бит из каждого байта
edx, xmm1
результата и перемещает в регистр edx
shr
edx, cl
shl
edx, cl
bsf
edx, edx
jnz
found
// находит старший единичный бит в регистре
loop:
26
add
eax,
16
movdqa
xmm1, [edx]
pcmpeqb xmm1, xmm0
pmovmskb
edx, xmm1
bsf
edx, edx
jz
loop
27
Жизненный цикл программ
Анализ
требова
ний
5%
ЖЦП – это период времени с момента идеи создания ПО до того, когда ПО перестает
быть доступным для пользователя.
Разработка
спецификаций
10%
Составление алгоритма и
кодирование программы
2,5%
Отладка в
условиях
эксперимента
7,5%
Целевая
компиляция и
настройка
1,5%
Автономная и
комплексная отладка.
Тестировние
20%
Документация
терожирование
внедрение
3,5%
Эксплуатация,
сопровождений,
модификация
50%
Модель ЖЦП определяет порядок этапов, подлежащих выполнению в ходе разработки и
развития ПО, а так же критерии перехода от этапа к этапу.
Этапы:
1. Анализ требований
На основе автоматизации какого-либо процесса производится формализация целей,
требований, критериев предъявляемых пользователем к определенному продукту.
2. Разработка спецификаций
Спецификация – это формализованное описание свойств и характеристик функций объекта
проектирования, разработанное в результате анализа.
Спецификация – это достаточно точное и полное описание задачи, которое человеку,
участвующему в решении, легче понять, чем программу на языке программирования.
Язык спецификаций – это язык более высокого уровня, чем язык кодирования для данной
задачи.
Спецификация бывает двух видов:
 На модули
 На функции
3. Составление алгоритма и кодирование программ
- это перевод спецификации на язык программирования.
4. Автономная и комплексная отладка
Тестирование – процесс констатации наличия ошибок.
Отладка – проверка соответствия модуля его спецификации.
Комплексная отладка – проведение тестирования программ, состоящих из модулей, с целью
выявления соответствия заданным входным параметрам спецификации.
28
5. Целевая и комплексная настройка универсальных программных модулей на
конкретные особенности системы
 Порты прерывания канала обмена
 Настройка на ОС
 Оптимизация по времени или памяти
6. Отладка в условиях эксперимента
При подключении к системе аппаратных средств окружения и при фиксированных в памяти
программных средств окружения, выполняется отладка в условиях эксперимента.
7. Собирается версия ПО, фиксируется на носителе, завершается процесс
документации.
8. Эксплуатация, сопровождений, модификация
Процесс, осуществляемый параллельно с эксплуатацией, направленный на обеспечение
требуемого качества ПО. Он заключается в выявлении и устранении незамеченных ранее
ошибок и модернизации системы при выявлении недостатков.
Типовой процесс разработки ПО по ГОСТу
1) Техническое задание (ТЗ)
 Определяются цели
 Выбирается метод решения задачи
 Проектируются алгоритмы
2) Эскизный проект
 Определяются структуры программных систем
 Определяются структуры модулей
 Происходит распределение ресурсов
 Разрабатываются спецификации для программных модулей
3) Технический проект
 Организация БД проекта
 Настройка средств проектирования на параметры целевых ЭВМ
 Разработка документа, в котором указаны порядок и сроки сдачи работ
заказчику
4) Рабочий проект
 Координирование
 Трансляция
 Контроль соответствия спецификациям
5) Отладка тестирования
6) Комплексно-динамическая отладка
7) Выпуск машинных носителей и документирование
8) Испытание комплекса программ
 Составление протокола испытаний
 Тестирование на соответствие спецификациям
29
Модели ЖЦП
I. Каскадная модель разработки программного продукта (70-е – 80-е).
Данная модель предполагает завершение каждого этапа ЖЦП перед началом
предыдущего.
испытания
тестирование
компановка
кодирование
разраб.спецификаций
анализ
Трудоемкость этапов:
II. Поэтапная итерационная модель.
Между этапами имеются циклы обратной связи
1
2
3
...
7
1.
2.
3.
4.
5.
6.
7.
Стратигическое планирование
Анализ требований
Проектирование
Разработка
Тестирование и отладка
Производство и внедрение
Сопровождение
30
Циклы обратной связи идут не по всему ххххх, а по этапу, что минимизирует затраты
этапа разработки. Толчком в интерации на предыдущий этап являются результаты
контроля промежуточных результатов.
Трудоемкость этапов:
1.
2.
3.
4.
Схема
Тестирование и отладка.
Разработка.
Проектирование
Анализ.
III. Спиральная модель (90-е)
Основная идея – идея конкуренции:
На каждом этапе производится проверка и обоснование данного этапа, а так же
существование альтернативных версий, из которых выбираются наиболее
соответствующие текущим планам.
Стратегическое
планирование
1
Интеграция
2
3
4
Анализ
Тестирование и
отладка
Реализация
1
2
Сопровождение
3
4
Проектирование
Переработка
1
2
3
4
1
Проектирование версии
2
Разработка версии
3
4
Тестирование+отладка версии
Производство версии
31
Классификация програмных средств, как объектов разработки
Показатель програмного средства
Высокий
Средний
Низкий
1. Сложность ( в тыс. команд)
2. Сложность ( в Кб)
3. Размер БД, использ. программой ( в Кб)
4. Связь с реальным временем – ограничение на
реакцию ( в сек.)
5. Надежность – среднее время наработки на
отказ ( в часах)
6. Степень используемых ресурсов ЭВМ ( в %)
7. Степень использования готовых компонентов
( в %)
8. Длительность эксплуатации ( в годах)
9. Тираж ( в экземплярах)
10. Степень документированности ( в страницах
на 1000 команд)
> 100 тыс.
> 700
> 5000
<1
> 10 тыс.
> 64
> 500
< 10
< 10 тыс.
< 64
< 500
> 10
> 100
> 10
< 10
> 90
> 50
> 70
> 20
< 70
< 20
> 10
> 1000
> 30
>2
> 100
> 10
<2
< 100
< 10
Переносимость программ
Программа называется переносимой из исходной вычислительной системы в целевую,
если она успешно компелируется в целевой и ее работа функциональна эквивалентной
работе в исходной вычислительной системе.
Изначально можно было говорить о переносимости языка Си, только в системе Unix.
Функция fork() – существует только в Unix (полноценную эмуляцию сделать
невозможно).
Факторы, влияющие на переносимость программ:
1.
2.
3.
4.
Аппаратные особенности компьютера и его рахитектура.
Особенности компилятора (алгоритмы).
Метрические ограничения компилятора.
Особенности ОС.
Все эти факторы учтены в ANSI, путем введения следующих типов поведения
программ:
I. Неуточняемое поведение – поведение правильных программ с корректными
входными данными в ситуациях, для которых стандарт не выдвигает никаких
требований.
II. Неопределенное поведение – поведение ошибочных программ с возможно
некорректными входными данными или объектами с неопределенными
значениями, для которых стандарт не выдвигает никаких требований.
III. Поведение, определяемой реализацией – поведение правильной программы с
корректными входными данными, которое зависит от реализации и должно
быть документировано в каждой реализации.
Чтобы программы была переносима необходимо:
I тип – избегать.
II тип – безусловно избегать.
III тип – минимизировать.
Пример конструкций в языке Си:
32
I тип: 1. Если выводим символ `\b` в начале строки - ? -> результат неизвестен.
2. Если используем, метод и время реализации статистических данных –
static – неизвестно, когда переменные будут инициализированы.
3. Представление плавающих типов данных – не стандартизировано.
4. Порядок и взаиморасположение памяти, выделяемое с помощью
функции malloc.
5. Порядок вычисления выражений и возникновение побочных эффектов.
Например, сумма a + b – реализуется путем вычисления сначала `a` или `b`, а затем `+`.
Если используем функцию - a( )+b( ), то возникают побочные эффекты:
int x;
a()
{
if (x) x++; // побочное выражения, очень важен порядок
return ( );
}
b()
{
if (x) x*=2; // побочное выражения, очень важен порядок
return ( );
}
II тип:1. Что будет, если вычислять разность указателей от разных массивов- ?
результат неизвестен.
2. Что будет, если вызываем функцию free( ) от указателя, который будет
получен не с помощью функции malloc..
3. Что будет при ссылке на память, освобожденную с помощью функции free( ).
4. Число формальных параметров не соответствует реальному.
III тип:1. Допустимы ли идентификаторы с символами отличными от стандартных
(например, использование в качестве переменных – русские быквы) ?
2. Различаются ли регистры в именах внешних переменных?
3. Существует ли файл нулевой длины? (стандарт об этом ничего не говорит).
4. Что будет при вызове функции malloc от нуля?
5. Надо ли заканчивать последнюю строку в программе символом перевода
строки \n? ( в Unix – «да», в Wind – «нет»).
6. Неизвестно будет ли операция знаковой, если она применяется к знаковым
типам данных.
7. Является ли тип char знаковым или безнаковым?
Примеры непереносимых конструкций языка Си:
Пример 1:
# define MASK 0xFF00
int x;
x&=MASK;
Предполагалось, что int имеет 2 байта. Если система 32-х битная, то будут обнуляться и все
ост.байты старше 2-х.
Решение:
- сдвинуть на 8 – вправо, затем – влево;
33
- переопределить маску как:
# define MASK (~0xFF) //обнуление происходит независимо от размера типа
данных int.
Пример 2:
Исходный код
Оптимизированный код
int
int
putchar (int c)
putchar (int c)
{
{
write (stdout,(char*)&c,1);
char d; d=(char)c;
}
write (stdout,&d,1);
}
Пример 3:
Исходный код
Оптимизированный код
char c;
unsigned char;
char table [256]
или
c=table [c];
c=table [(unsigned char) c];
Пример 4:
Исходный код
Оптимизированный код
int isrus (char c);
int isrus (unsigned char c);
{
….
if (c<128) return (0);
if (c>=’a’&&cc<=’я’);
return (1);
return (0);
}
Пример 5:
Исходный код
char c;
while ((c=getchar())!=EoF)
{
…..
Оптимизированный код
int c;
EoF – «конец файла»чаще всего ‘-1’, поэтому
некоторые коды могут воспринимать ‘-1’ как
конец файла
}
34
Пример 6:
Исходный код
char c;
if (c>=’a’&&c<=’z’)
Оптимизированный код
используем функцию языка Си:
islower (c);
{
…
}
Какие правила необходимо использовать в программе, чтобы она была переносимой ?
Метрические ограничения компиляторов
(Стандарт ANSI)
 15 уровней вложенностей операций;
 6 уровней сложности предпроцессорных директив;
 127 уровней скобок;
 31 символ в имени идентификатора;
 6 символов в имени внешнего идентификатора
 31 параметр у функций или макроопределений;
 509 символов в строке программы;
 509 символов в строковой переменной const(‘…..’).
Тестирование и отладка
Отладка – искусство локализации ошибок, когда факт их наличия установлен.
Отладка и тестирование – это процесс, позволяющий получить программу с требуемыми
характеристиками, функционирующую в заданной области входных данных.
Различия между отладкой и тестированием и отладкой:
1. Тестирование – устанавливает факт наличия ошибки,
отладка – факт устранения ошибки.
2. Отладка - ведется в предвиденье неправильной работы программы,
Тестирование – предвиденье правильной работы программы.
3. Мало отладить программу, ее необходимо еще и оттестировать!
На сегодняшний день существует два подхода к тестированию и отладке:
1. Формальный, например, инвариант числа (задача решается только полным перебором,
что практически нереально).
2. Интерпритационный, проверяем истинность на некотором множестве входных
данных, следовательно можем установить только факт наличия ошибки, а не факт
отсутствия.
Невозможность исчерпывающего тестирования
Пример: пусть мы ничего не знаем о функции «черный ящик»
35
int
Blackbak ()
int
int
2^32 * 2^32 = 2^64
Взламывали DES – 2^56 - 10 лет назад считали в течении 1 недели или месяца.
2^ 64 невозможно проверить т.к. необходимо слишком много мощности и слишком много
памяти для хранения пар значений.
Если функция простая, то лучше представить ее как «белый ящик»:
Например: ‘+’
+
Вывод: Нужно тестировать только спорные и граничные значения.
Если добавить условие, что делать N раз, то результат близок к 2^64
36
X
Y
A
N
B
Z
Вывод: исчерпывающее тестирование невозможно,
следовательно необходимо найти множество тестов, которые тестируя нашу функцию
определяют наибольшее количество ошибок.
Критерии выбора тестов:
1. Достаточность (набор тестов достаточен для нахождения всех ошибок) – невыпонимо.
2. Надежность (тесты должны выявлять эти ошибки всегда, а не только в некоторых
случаях).
3. Легкая проверяемость.
Все эти критерии являются протеворечивыми, поэтому необходимо находить компромисы –
классы критериев т.е. выбирать нужные тесты:
I. Структурный класс критериев (соответствует структурному тестированию).
II. Функциональный класс критериев.
III. Стахостичесткий класс критериев.
IV. Мутационный класс критериев.
I.
Структурный класс критериев.
Путь – последовательность путей и дуг управляющего графа (графа передачи управления).
Ветвь – это путь (V1….Vn), где V1 и Vn – условные или конечные операторы, а все Vk – это
безусловные операторы.
Подклассы:
 С0 – тестирование команд (выбираем набор тестов так, чтобы любая команда
проходилась не менее одного раза).
 С1 – тестирование ветвей (прохождение любой ветви не менее одного раза).
 С2 – тестирование путей (прохождение любого пути не менее одного раза).
Пример:
37
1. void A(x) { int x
2.
if (x>17);
3.
x=17-x;
4.
if (x==-13);
5.
x=0;
6.
return(x); }
Граф представлен на рисунке ниже:
1
2
3
4
5
6
Вершины: 1-6.
Ветви: (1,2),(2,3,4),(2,4),(4.5.6),(4,6).
Пути: (1,2,4,6),(1,2,3,4,6),(1,2,4,5,6),(1,2,3,4,5,6).
Находим набор тестов для С0
С0 = {(30,0)}
С1 = {(30,0),(17,17)}
Ветви: (1,2), (2,3,4), (2,4), (4,5,6), (4,6)
С2 = {(30,0),(17,17),(18,-1),(-13,0)}
Пути: (1,2,4,6), (1,2,3,4,5,6), (1,2,3,4,6), (1,2,4,5,6)
Вычеркиваем те пути, которые прошли раньше, затем надо написать дополнительный тест,
который пройдет оставшиеся пути.
Выводы: Критерий С2 - самый мощный.
Структурный критерий может быть легко автоматизирован (программа может построить
граф и подобрать значения).
Вопрос: Когда структурный критерий может ошибаться?
II.
Функциональное тестирование.
(Тестирование близкое к «черному ящику», хотим подобрать тесты, чтобы найти ошибку)
Не имеем исходных тестов функций, но имеем её спецификацию (входные\выходные
параметры, алгоритм, может быть приведен пример).
Как сделать выбор:
1. Тестирование класса входных данных (типа, подтипы и особые режимы). Особое
внимание уделять граничным условиям.
2. Тестирование класса выходных данных (спецификация может не соблюдаться, а мы
хотим получить все возможные варианты выходных данных, и вариант аварийного
выхода).
38
3. Тестирование функций – каждое действие, описанное в спецификации проверяется по
крайней мере 1 раз.
Пример: Посчитать диагональ по трем сторонам.
d
a
c
b
Известно: int a,b,c > 0
float d
d=sqrt(a^2+b^2+c^2)
Тестируем:
Входные данные –
Подаем на вход (1,1,1)
(2,10,30)
Граничные значения –
(1,0,2) (0,2,3) (2,5,0)
(1,0,0) (0,0,5) (0,6,0)
(0,0,0)
 выводить error
 выводить error
 выводить либо error, либо «0»
(MAX_INT) ( ) ( )
( )( )( )
( )( )( )
(MAX_INT – 1) ............
Проверяем переполнение в корнях
?????
Тестирование на закладки, вирусы и т.д.
(доп.информация в статье Ken Tompson “Reflections on trusting trust”)- или как спрятать в
программе на Си «троянского коня»?
1. Пусть есть функция –
login (char * pass)
{ if ( hash (pass==…)return(1));
return (0);
…}
2. Пусть есть функция –
complete (char * str)
if (strcmp(str,’login’)==0) tk();
3. Внедряем второго «троянского коня»
39
TK1
TK2
«Чистый» компилятор => «чистая» функция login!
Вывод: По анализу исходных тестов невозможно найти «троянского коня».
III.
Стахостический класс критериев.
Пытается найти разумный предел по времени, когда необходимо заканчивать тестирование.
Когда заканчивать тестирование:
1) Час Х.
2) Когда нет ошибок (набор некоторых тестов дает результат).
3) Когда ошибок n зведомо, меньше большого числа N (например, одна ошибка на 10
тысяч команд).
Строится аппроксимация ошибок в программе.
Предполагаетя, что количество ошибок изменится экспоненциально.
Выбираем некоторый уровень ошибок.
N
No
to
T
Затем аппроксимируем
N
N1
N2
40
N3
t1 t2
IV.
t3
Мутационное тестирование.
Критерий определения правильности тестов.
Мы постулируем – «Профессионалы пишут почти правильные программы», отличающиеся
от правильных лишь:
 ошибки в min/max индексах массива;
 ошибки знаков операций;
 ошибки типа + 1;
Такие прогаммы будем называть мутациями.
Если в тестовую программу вносятся случайные мутации, и набор тестов выявляет все
мутационные ошибки, то набор тестов признается соответствующим мктациооному
критерию.
Пример:
Char [M];
i = i+1; i = i-1;
for (i=0; i<N;i++);
i<N-1;
Тестирование модулей
M
Основнпя программа
M1
M11
M2
M12
M21
другие модули
M22
другие модули
Два подхода тестирования модулей:
1. Восходящее (начинают с последних модулей, которые ничего не вызывают, далее
идем на уровень выше).
2. Нисходящее (начинают тестирование с главной программы).
Нисходящее тестирование:
41
«+» - в первую очередь будут тестироваться связи между модулями;
- можно вести тестирование параллельно с разработкой, пока нижелещащие модули еще
не готовы. Если какой-то модуль еще не готов, то используют ‘заглушки’, дающие
заведомо верный результат.
«-» - необходимость заглушек.
Восходящее тестирование:
«+» - не нужно заклушек;
- если находим ошибку в каком-то модуле, это означает, что ошибка именно в нем, т.к.
модули ниже уже проверены;
- тестирование можно вести параллельно с разработкой модулей;
«-» - связи между модулями тестируются в последнюю очередь.
Сложность. Её источники.
Откуда возникают ошибки:
1. Разрыв между потенциальными возможностями компьютера и уровнем и хар-ром
операции для предоставления какой-либо услуги.
2. Отсутсвие у компьютера модели реального мира, и возможности интерпретировать
планы, имеющиеся у программиста.
3. Требования абсолютной точности (детальности) плана (заказчик не может точно
изложить требования).
4. Рассогласованность модели проблемной области, которая существует у программиста
и может быть реализована в компьютере.
Методом борьбы со сложностью в современных языках програмирования являются средства
абстракции и конкретизации:
a.
b.
c.
d.
Сверстка операций (возможность вызова подпрограммы, понятие модуля).
Сверстка данных (данные должны быть типизированы)
Свертка результатов (ограничение контекста, в котором могут вызываться модули).
Понятие объекта.
Сравнение средств борьбы со сложностью в различных языках програмирования:
1. Сверстка операций (call)
2. Типы
3. Сверстка ресурсов (контекст)
4. Механизмы органичений
 подтипы
 области видимости
 перегрузка
 наследование
 раздельная компиляция
5. Механизмы контроля
 синтаксический
 контроль типов
 контроль контекстов
 обработка исключений
ASM
Pasсal
C++
+
-
+
++
-
+
+
+
+
+
+
-/+
+
+
+
+
+
+
-
+
+
+
+
+(-)
+
+
+
42

верификация
43
Универсальные вычисления
на графических процессорных устройствах GPGPU
1. CUDA (NVIDIA).
2. Brook (AMD).
3. Open CL.
Закон Мура – каждые два года в мире происходит удвоение количества транзисторов на
процессовах (ранее этот процесс вызывал и удвоение мощности и скорости вычислений,
но сейчас, это уже невозможно, например, существует ограничение скоростью света).
Сейчас идет процесс увеличения количество ядер в процессоре (например, 4-х и 8-ми
ядерные процессоры).
Решение данной проблемы «пришло» из оптимизации компьютерных игр, которые со
временем требовали все больше и больше ресурсов.
Было предложено выведение изображения (каждый шейдер отвечает за свою часть
экрана):
Схема
Возник вопрос – Почему бы не использовать шейдеры для GPGPU?
В современных графических устройствах 200 или 800 шейдеров)
CUDA
Терминология:
1) Хост (host) – CPU.
2) Устройство (device) – GPU.
3) Поток (thread) – основная испольняемая единица на GPU.
4) Варп (warp) – min набор потоков для испольнения (в текущей архитектуре – это 32
потока).
5) Блок (block) – одно/двух/трехмерное объединение потоков, взаимодействующие
между собой.
6) Сеть (grid) – набор блоков.
Рис. Разбиение исходной задачи на набор независимо решаемых подзадач
7) Ядро (kernel) – процедура, испольняемая на устройстве и запускаемая с хоста.
Основное доваление к языку Си в Cuda – А<<<512,64>>>(параметры).
44
Модель програмирования, которая отвечает за .................
Отличие програмной модели CPU и GPU
1) На GPU можно запускать гораздо больше потоков, чем на CPU (до 300
тыс.потоков). Переключение на GPU между потоками очень простое и занимает
один такт.
2) Все ядра GPU имеют технологию SIMD – одна инструкция, много данных, MIMD –
много инструкций, много\мало данных???
3) Ядра GPU намного проще, меньше инструкций, нет сложных блоков упр.
4) На GPU различают разные уровни памяти (доступ к памяти чаще всего близок к
последовательному и нет большого кэш’а).
Содержимое кристалла CPU и GPU:
45
Модель памяти в CUDA
1. Регистровая.
Мах быстрый доступ, регистры могут быть доступны только из одного потока
(регистров в GPU намного больше, чем в CPU; у одного потока ~ 32/64 регистра).
2. Разделяемая (shared) память.
Очень быстрая память, область видимости – блок (все потоки в одном блоке имеют
доступ к одной раздел памяти на 1 блок – 6 – 64 Кб).
3. Локальная память.
Очень медленная, доступ внутри одного потока (доступ к ним до 100 раз больше, чем
к регистровой).
4. Глобальная (global) память.
Очень медленная (все потоки имеют к ней доступ).
5. Константная (constant) память.
Быстрая, доступ к ней имеют все потоки, но в режиме read-only.
Пример: (цикл складывает два массива)
Си:
int A[N],B[N],C[N]
for (i=0;i<N;i++)
A[i]=B[i]+C[i];
Global_void Add (int*A, int*B, int*C, int N)
{
int i; // номер потока, который вычисляем
i=blockIDx.x*blockDim.x+threadIDx.x;
//blockIDx.x - номер блока
//blockDim.x – размерность блока
//threadIDx.x – номер нити
if (i<N);
A[i]=B[i]+C[i];
}
Напишем вызывающую процедуру, которая будет выполнена на CPU:
int main()
{
const int N=1024:
int *a_h, *b_h, *c_h; // host
int *a_d, *b_d, *c_d; // device
// выделяем память для данных, которые будут на хосте
46
a_h=(int*)malloc(sizeof(int)*N);
b_h=(int*)malloc(sizeof(int)*N);
c_h=(int*)malloc(sizeof(int)*N);
// выделяем память для данных, которые будут на device
cudaMalloc((void**)&a.d,sizeof((int)*N);
cudaMalloc((void**)&b.d,sizeof((int)*N);
cudaMalloc((void**)&c.d,sizeof((int)*N);
//инициализируем масствы В,С на host
for (i=0;i<N;i++)
{
[i]=rand():
[i]=rand();
}
// передаем задачу
CudaMemcpu(a_d,a_h,sizeof(int)*N,CudaMemcpu Host to Device);
CudaMemcpu(b_d,b_h,sizeof(int)*N,CudaMemcpu Host to Device);
CudaMemcpu(c_d,c_h,sizeof(int)*N,CudaMemcpu Host to Device);
// можем запустить ядро
Add<<<N/64,64>>>(a_d,b_d,c_d,N);
//синхронизация
CudaThreadSyncronize();
// ждем пока ядро не отработает
// получаем результаты с device
CudaMemcpu(a_h,a_d,sizeof(int)*N,CudaMemcpu Device to Host);
// от всех массивов
Free();
CudaFree();
}
Характеристики CUDA:
1. CUDA = Compute Unified Device Architecture
2. Для установки Cuda необходимы:
 драйвер, который поддерживает Cuda;
 скачать toolkit, который включает в себя:
i. компилятор nvcc:
ii. библиотека (например: Cudart40_80.dll);
iii. CuBLAS(стандартная библиотека с дополнительными вычислениями);
iv. driver API;
v. профайлер.
Поддержка Studio – до версии 3.0 – исполнение в режиме эмуляции (т.е.
выполнение в режиме CPU на 1 ядре и последовательно), но зато в эмуляторе
удобно тестировать, например, используя breakpoint
3. SDK.
Пример программы:
strlen с помощью CUDA:
shar str[MAY_SIZE]
_global_void strlen(char*s,int*len)
47
{
int i=blockIdx.x*blockDim.x+threadIdx.x;
if (i<MAX_SIZE)
if (s[i]==’10’
if (i<*lim)*len=i; // лучше atomicMin(len,i);
}
Атамарные функции:
atomicAdd (int*addr,int val);
atomicSub (int*addr,int val);
atomicExch (int*addr,int val);
atomicMin (int*addr,int val);
atomicMax (int*addr,int val);
atomicInc (int*addr,int val);
atomicDec (int*addr,int val);
atomicCAS (int*addr,int compare, int val); //CAS – CompareAndSwap – сравнить и поменять
Псевдо код:
{
int dd=*addr;
*addr=(dd==compare)?
val:old;
}
Использование разделяемой памяти
Разделяемая память доступна всем нитям в одном блоке, поэтому попробуем уменьшить
доступ к глобальной памяти используя разделяемую.
Пример: перемножение матриц
Чтобы повысить быстродействие программы за счет использования shared-памяти.
Нужно разбить результирующую матрицы на подматрицы 16*16, вычислением каждой
такой подматрицы будет заниматься один блок. Обратите внимание, что для вычисления
такой подматрицы нужны только небольшие "полосы" матриц A и B (см. рис. 1).
48
Рис 1. Части матриц A и B, используемые для вычисления подматрицы C.
К сожалению целиком копировать эти "полосы" в shared-память практически нереально изза довольно небольшого объема shared-памяти. Поэтому можно поступить другим образом разобьем эти "полосы" на матрицы 16*16 и вычисление подматрицы произведения матриц
будем проводить в N/16 шагов.
Для этого обратим внимание, что расчет элемента ci,j можно переписать следующим образом,
используя разбиение полос на квадратные подматрицы
c [i][j] = 0
for step in 0..N/16:
for k in 0..16:
c [i][j] += a [i][k+step*16] * b [k+step*16][j]
Обратите внимание, что для каждого значения step значения из матриц A и B берутся из двух
подматриц размером 16*16. Фактически полосы с рис. 4 просто поделены на квадратные
подматрицы и каждому значению step соответствует по одной такой подматрице A и одной
подматрице B (см. рис. 2).
49
Рис 2. Разбиение полос матриц A и B на подматрицы 16*16.
На каждом шаге будем загружать в shared-память по одной 16*16 подматрице A и одной
16*16 подматрице B. Далее будем вычислять соответствующую им сумму для элементов
произведения, потом загружаем следующие 16*16-подматрицы и т.д.
При этом на каждом шаге одна нить загружает ровно по одному элементу из каждой из
матриц A и B и вычисляет соответствующую им сумму членов. По окончании всех
вычислений производится запись элемента в итоговую матрицу.
Обратите внимание, что после загрузки элементов из A и B нужно выполнить
синхронизацию нитей при помощи вызова __synchronize для того, чтобы к моменту начала
расчетов все нужные элементы (загружаемые остальными нитями блока) были бы уже
загружены. Точно также по окончании обработки загруженных подматриц и перед загрузкой
следующих также нужна синхронизация (чтобы убедится что текущие 16*16 подматрицы
больше не нужно и можно загружать новые).
Ниже приводится соответствующий исходный код.
#include <stdio.h>
#define BLOCK_SIZE
#define N
16
1024
// submatrix size
// matrix size is N*N
__global__ void matMult ( float * a, float * b, int n, float * c )
{
int bx = blockIdx.x;
// block index
int by = blockIdx.y;
int tx = threadIdx.x;
int ty = threadIdx.y;
// thread index
50
// Index of the first sub-matrix of A processed
by the block
int aBegin = n * BLOCK_SIZE * by;
int aEnd = aBegin + n - 1;
// Step size used to iterate through the submatrices of A
int aStep = BLOCK_SIZE;
// Index of the first sub-matrix of B processed
by the block
int bBegin = BLOCK_SIZE * bx;
// Step size used to iterate through the submatrices of B
int bStep = BLOCK_SIZE * n;
float sum = 0.0f;
// computed subelement
for ( int ia = aBegin, ib = bBegin; ia <= aEnd; ia += aStep, ib += bStep )
{
// Shared memory for the sub-matrix of A
__shared__ float as [BLOCK_SIZE][BLOCK_SIZE];
// Shared memory for the sub-matrix of B
__shared__ float bs [BLOCK_SIZE][BLOCK_SIZE];
// Load the matrices from global memory to shared
memory;
as [ty][tx] = a [ia + n * ty + tx];
bs [ty][tx] = b [ib + n * ty + tx];
__syncthreads();
// Synchronize to make sure the matrices are loaded
// Multiply the two matrices together;
for ( int k = 0; k < BLOCK_SIZE; k++ )
sum += as [ty][k] * bs [k][tx];
// Synchronize to make sure that the preceding
// computation is done before loading two new
// sub-matrices of A and B in the next iteration
__syncthreads();
}
// Write the block sub-matrix to global memory;
// each thread writes one element
int ic = n * BLOCK_SIZE * by + BLOCK_SIZE * bx;
c [ic + n * ty + tx] = sum;
}
int main ( int argc, char * argv [] )
{
int numBytes = N * N * sizeof ( float );
// allocate host memory
float * a = new float [N*N];
float * b = new float [N*N];
float * c = new float [N*N];
for ( int
for (
{
a
b
}
i = 0; i < N; i++ )
int j = 0; j < N; j++ )
[i] = 0.0f;
[i] = 1.0f;
// allocate device memory
float * adev = NULL;
51
float * bdev = NULL;
float * cdev = NULL;
cudaMalloc ( (void**)&adev, numBytes );
cudaMalloc ( (void**)&bdev, numBytes );
cudaMalloc ( (void**)&cdev, numBytes );
// set kernel launch configuration
dim3 threads ( BLOCK_SIZE, BLOCK_SIZE );
dim3 blocks ( N / threads.x, N / threads.y);
// create cuda event handles
cudaEvent_t start, stop;
float gpuTime = 0.0f;
cudaEventCreate ( &start );
cudaEventCreate ( &stop );
// asynchronously issue work to the GPU (all to stream 0)
cudaEventRecord ( start, 0 );
cudaMemcpy
( adev, a, numBytes, cudaMemcpyHostToDevice );
cudaMemcpy
( bdev, b, numBytes, cudaMemcpyHostToDevice );
matMult<<<blocks, threads>>> ( adev, bdev, N, cdev );
cudaMemcpy
( c, cdev, numBytes, cudaMemcpyDeviceToHost );
cudaEventRecord ( stop, 0 );
cudaEventSynchronize ( stop );
cudaEventElapsedTime ( &gpuTime, start, stop );
// print the cpu and gpu times
printf("time spent executing by the GPU: %.2f millseconds\n", gpuTime );
cudaEventDestroy
cudaEventDestroy
cudaFree
cudaFree
cudaFree
//
(
(
(
(
(
release resources
start );
stop );
adev );
bdev );
cdev );
delete a;
delete b;
delete c;
return 0;
}
Теперь для вычисления одного элемента произведения матриц нам нужно всего 2*N/16
чтений из глобальной памяти. И по результатам сразу видно за счет использования
shared-памяти нам удалось поднять быстродействие более чем на порядок.
Оптимизация программ на CUDA
1. Повышение параллельности алгоритма.
2. Улучшение использования ресурсов GPU.
3. Уменьшение использования медленной памяти (локальной и глобальной).
I. Оптимизация пересылок между CPU и GPU:
52



По возможности вычислять данные на GPU;
Использовать асинхронные операции, вместо
CudaMemcpu<->CudaMemcpuAsync;
Использовать специальные механизмы, которые есть в последних версиях
GPU от NVidia (чтобы выделить память хосту и устройству). Zero-copy –
копирование с нулевой задержкой.
II. Оптимизация памяти на GPU:





Использовать разделяемую память (shared memory):
Увеличить количество используемых регистров (чем больше количество
регистров на потоке, тем меньше потоков);
Минимизировать количество CudaMalloc и CudaFree;
По возможности использовать coalescing доступ к памяти;
Активно использовать constant memory.
III. Настройки выполнения «нашего» ядра:
 Число нитей должно быть кратно 32;
 Для разрешения зависимостей между регистрами количество нитей
должно быть достаточно большим (192, 256, ... );
Таблица ограничений (по версиям Cuda):
1.0 – 1.1
1.2 – 1.3
2.0
Регистры на мультипроцессор
8192
16384
32768
Мах возможное кол-во потоков
768
1024
2048
Разделянмая память
16 Кб
16 Кб
48 Кб
Для подбора параметров существует функция cuda_occupancy_calculator.xls
(на входе количество регистров, потоков – на выходе занятость CPU).
IV. Оптимизация инструкций:



Использовать вместо sinf() < -- > __sinf();
Снижение мощности;
Сдвиги вместо деления.
V. Оттимизация переходов:
if ( )
{
} // если все потоки идут по одному пути, то это не страшно
else
{
} // если потоки идут по разным путям, то теряем время при
//синхронизации. Следовательно необходимо избегать различных путей
//выполнения внутри одного warp’s.
VI. Вызовы функций:
В первых двух версиях не было вызовов функций, следовательно все функции
были inline (это хорошо влияет на время выполнения кода). В версии 2.0 эта
функция появилась, но по возможности надо использовать inline функции в
коде нашего ядра (т.к. не было функций, то не было и рекурсии).
VII. Развертка циклов:
53


#pragma unroll N // развернет цикл в N раз;
for ( ………. )
Лучше использовать в заголовках цикла int вместо unsigned int.
VIII. Другие оптимизации:

Для ядер с большим числом аргументов использовать некоторые из них в
constant memory, чтобы увеличить использование разделяемой памяти.
 Избегать преобразования из double во float.
Как исключить функцию syncthreads
int Array [….]
_global void A(int*res)
{
tid=theadIdx.x; // считаем в каждой нити значения массива
int ref1=myArray[tid];
- syncthread();
myArray[tid+1]=2: // изменяем массив
- syncthead();
int ref2=myArray[tid];
res[tid]=ref1*ref2;
}
Если известно, что все нити внутри одного warp, то можно переписать код:
volatile int myArray[ ]
_lobal void A(int*res)
{
int tid=threadIdx.x;
if (tid<warp size) // 32
{
int ref1=myArray[tid];
myArray-tid+1]=2;
int ref2=myArray[tid];
res[tid]=ref1*ref2;
}
}
54
OpenCL (Open Computing Language)
Програмная модель памяти:
Отличие OpenCL от CUDA – это появление очередей. Всё, что запускается от Computer
Device попадает в очередь.
Сопоставление терминов CUDA и OpenCL:
CUDA
OpenCL
thread
block
global m
constant m
shared m
local m
_global_ ()
_device_ ()
work_item
work_group
global memory
constant memory
local memory
private memory
_Kernel
любая, у кот.нет спецификатора
55
_constant_
_device_
_shared_
_syncthreads_ ()
gridDim
blockDim
blockIdx
threadIdx
_constant
_global
_local
_barrier ()
get_mem_group ()
get_local_size ()
get_group_id ()
get_local_id ()
blockIdx*
blockDim+threaded
get_global_id ()
gridDim+blockDim
get_global_size ()
Сравним алгоритм суммирования двух массивов на CUDA и
OpenCL
На CUDA:
__global__void vectorAdd( const float* a,
const float* b,
float* c)
{
int tid = blockIdx.x*blockDim.x+threadIdx.x;
c[tid] = a[tid] + b[tid];
}
const unsigned int cnBlockSize = 512;
const unsigned int cnBlocks = 3;
const unsigned int cnDimension = cnBlocks * cnBlockSize;
CUdevice hDevice;
CUcontext hContext;
CUmodule hModule;
CUfunction hFunction;
// create CUDA device & context
cuInit(0);
cuDeviceGet(&hContext, 0); // pick first device
cuCtxCreate(&hContext, 0, hDevice));
cuModuleLoad(&hModule, “vectorAdd.cubin”);
cuModuleGetFunction(&hFunction, hModule, "vectorAdd");
// allocate host
float * pA = new
float * pB = new
float * pC = new
vectors
float[cnDimension];
float[cnDimension];
float[cnDimension];
// initialize host memory
randomInit(pA, cnDimension);
56
randomInit(pB, cnDimension);
// allocate memory on the device
CUdeviceptr pDeviceMemA, pDeviceMemB, pDeviceMemC;
cuMemAlloc(&pDeviceMemA, cnDimension * sizeof(float));
cuMemAlloc(&pDeviceMemB, cnDimension * sizeof(float));
cuMemAlloc(&pDeviceMemC, cnDimension * sizeof(float));
// copy host vectors to device
cuMemcpyHtoD(pDeviceMemA, pA, cnDimension * sizeof(float));
cuMemcpyHtoD(pDeviceMemB, pB, cnDimension * sizeof(float));
// set up parameter values
cuFuncSetBlockShape(cuFunction, cnBlockSize, 1, 1);
#define ALIGN_UP(offset, alignment) \
(offset) = ((offset) + (alignment) – 1) & ~((alignment) – 1)
int offset = 0;
ALIGN_UP(offset, __alignof(pDeviceMemA));
cuParamSetv(cuFunction, offset, &ptr, sizeof(pDeviceMemA));
offset += sizeof(pDeviceMemA);
ALIGN_UP(offset, __alignof(pDeviceMemB));
cuParamSetv(cuFunction, offset, &ptr, sizeof(pDeviceMemB));
offset += sizeof(pDeviceMemB);
ALIGN_UP(offset, __alignof(pDeviceMemC));
cuParamSetv(cuFunction, offset, &ptr, sizeof(pDeviceMemC));
offset += sizeof(pDeviceMemC);
cuParamSetSize(cuFunction, offset);
// execute kernel
cuLaunchGrid(cuFunction, cnBlocks, 1);
// copy the result from device back to host
cuMemcpyDtoH((void *) pC, pDeviceMemC,
cnDimension * sizeof(float));
delete[] pA;
delete[] pB;
delete[] pC;
cuMemFree(pDeviceMemA);
cuMemFree(pDeviceMemB);
cuMemFree(pDeviceMemC);
На OpenCL:
__kernel void vectorAdd(__global const float * a,
__global const float * b,
__global float * c)
{
// Vector element index
int nIndex = get_global_id(0);
c[nIndex] = a[nIndex] + b[nIndex];
}
// Kernel launch configuration
const unsigned int cnBlockSize = 512;
const unsigned int cnBlocks = 3;
const unsigned int cnDimension = cnBlocks * cnBlockSize;
// Get OpenCL platform count
cl_uint NumPlatforms;
clGetPlatformIDs (0, NULL, &NumPlatforms);
57
// Get all OpenCL platform IDs
cl_platform_id* PlatformIDs;
PlatformIDs = new cl_platform_id[NumPlatforms];
clGetPlatformIDs(NumPlatforms, PlatformIDs, NULL);
// Select NVIDIA platform (this example assumes it IS present)
char cBuffer[1024];
cl_uint NvPlatform;
for(cl_uint i = 0; i < NumPlatforms; ++i)
{
clGetPlatformInfo (PlatformIDs[i], CL_PLATFORM_NAME, 1024, cBuffer, NULL);
if(strstr(cBuffer, "NVIDIA") != NULL)
{
NvPlatform = i;
break;
}
}
// Get a GPU device on Platform (this example assumes one IS present)
cl_device_id cdDevice;
clGetDeviceIDs(PlatformIDs[NvPlatform], CL_DEVICE_TYPE_GPU, 1,
&cdDevice, NULL);
// Create a context
cl_context hContext;
hContext = clCreateContext(0, 1, &cdDevice, NULL, NULL, NULL);
// Create a command queue for the device in the context
cl_command_queue hCmdQueue;
hCmdQueue = clCreateCommandQueue(hContext, cdDevice, 0, NULL);
// Create & compile program
cl_program hProgram;
hProgram = clCreateProgramWithSource(hContext, 1, sProgramSource, 0, 0);
clBuildProgram(hProgram, 0, 0, 0, 0, 0);
// Create kernel instance
cl_kernel hKernel;
hKernel = clCreateKernel(hProgram, “vectorAdd”, 0);
// Allocate host
float * pA = new
float * pB = new
float * pC = new
vectors
float[cnDimension];
float[cnDimension];
float[cnDimension];
// Initialize host memory (using helper C function called “randomInit”)
randomInit(pA, cnDimension);
randomInit(pB, cnDimension);
// Allocate device memory (and init hDeviceMemA and hDeviceMemB)
cl_mem hDeviceMemA, hDeviceMemB, hDeviceMemC;
hDeviceMemA = clCreateBuffer(hContext,
CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
cnDimension * sizeof(cl_float), pA, 0);
hDeviceMemB = clCreateBuffer(hContext,
CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
cnDimension * sizeof(cl_float), pB, 0);
hDeviceMemC = clCreateBuffer(hContext,
CL_MEM_WRITE_ONLY,
cnDimension * sizeof(cl_float), 0, 0);
// Setup parameter values
clSetKernelArg(hKernel, 0, sizeof(cl_mem), (void *)&hDeviceMemA);
clSetKernelArg(hKernel, 1, sizeof(cl_mem), (void *)&hDeviceMemB);
58
clSetKernelArg(hKernel, 2, sizeof(cl_mem), (void *)&hDeviceMemC);
// Launch kernel
clEnqueueNDRangeKernel(hCmdQueue, hKernel, 1, 0, &cnDimension, 0, 0, 0, 0);
// Copy results from device back to host; block until complete
clEnqueueReadBuffer(hContext, hDeviceMemC, CL_TRUE, 0,
cnDimension * sizeof(cl_float), pC, 0, 0, 0);
// Cleanup
delete[] pA;
delete[] pB;
delete[] pC;
delete[] PlatformIDs;
clReleaseKernel(hKernel);
clReleaseProgram(hProgram);
clReleaseMemObj(hDeviceMemA);
clReleaseMemObj(hDeviceMemB);
clReleaseMemObj(hDeviceMemC);
clReleaseCommandQueue(hCmdQueue);
clReleaseContext(hContext);
59
Download