Upload
others
View
22
Download
0
Embed Size (px)
Citation preview
Модели многопоточного программирования (TBB, OpenMP)
Алексей Федотов
SSG/DPD/TCAR/Threading Runtimes
22.02.2017
Мотивация
“The free lunch is over: A Fundamental Turn Toward Concurrency” – Herb Sutter, Dr. Dobb’s журнал, март 2005 г.
2
10 ГГц
1 ГГц
100 МГц
10 МГц
1 МГц
’79 ’87 ’95 ’03 ’11
Частота процессора
больше
не возрастает
… но мы хотим, чтобы
приложения исполнялись
быстрее…
Когда нет супергероя, на помощь приходят миллионы рядовых солдат!
Уровни параллелизма(аппаратное обеспечение)
• Параллелизм на уровне инструкций (ILP)• Конвейерное исполнение.
• Супер-скалярное исполнение (Hyper-Threading).
• Параллелизм на уровне данных (DLP)• SIMD (Single Instruction Multiple Data) векторная обработка.
• Реализован через использование SSE регистров и инструкций.
• Параллелизм на уровне потоков (TLP)• Многоядерная архитектура.
• Множество сокетов с когерентной кэш-памятью.
• Параллелизм на уровне кластеров (CLP)• Множество платформ, соединённых через сеть.
• Нет аппаратный поддержки когерентности кэш-памяти.
3
Уровни параллелизма(программное обеспечение)
• Передача сообщений
• Обработка событий
• Параллелизм данных
• Вектора, SIMD
• Параллелизм данных
• Параллелизм задач
• Шаблон fork-join
Неструктурированный
Структурированный
Структурированный
Все 3 уровня необходимы чтобы достигнутьмаксимального параллелизма
4
5
void AddHead (struct List *list,
struct Node *node) {
node->next = list->head;
list->head = node;
}
Сработает на двух потоках?
6
int flag = 0;
void AddHead (struct List *list,
struct Node *node) {
while (flag != 0) /* wait */ ;
flag = 1;
node->next = list->head;
list->head = node;
flag = 0;
}
Сработает на двух потоках?
7
T10
flagT2
1
flag
0
flag
Гонки данных
Пример: i++;
8
Поток №1 Поток №2Общийсчётчик
0
Чтение 0
Сложение 0
Запись 1
Чтение 1
Сложение 1
Запись 2
Поток №1 Поток №2Общийсчётчик
0
Чтение 0
Чтение 0
Сложение 0
Сложение 0
Запись 1
Запись 1
Чем гонки данных так неприятны?
Программы, в которых есть гонки данных, проявляютнедетерминированность в своём исполнении:
• Иногда дают верный результат
• Иногда дают ошибочный результат
Такие программы обычно работают корректно намаленьком количестве потоков с небольшим наборомданных.
Проблемы проявляются чаще, когда количествопотоков и время выполнения программы возрастает.
9
Отладка таких программ затруднительна!
Критические секции
• Это участки кода, которые выполняются потоками эксклюзивно.
• Реализованы при помощи примитивов синхронизации:
• pthread_mutex (Linux*)
• CRITICAL_SECTION (Windows*)
10
Критические секции устраняют гонки данных!
Критические секции исполняются последовательно!
Нужно понять, что должно исполняться эксклюзивно!
Взаимная блокировка
Потоки ждут некоторого события или условия, которое никогда не произойдёт.
Пример: дорожные пробки.Машины не могут повернуть или развернуться.
11
12
Взаимная блокировка (пример)
CRITICAL_SECTION cs1;
CRITICAL_SECTION cs2;
int x = 0;
int y = 0;
InitializeCriticalSection(&cs1); // Allocation Site (cs1)
InitializeCriticalSection(&cs2); // Allocation Site (cs2)
EnterCriticalSection(&cs1);
x++;
EnterCriticalSection(&cs2);
y++;
LeaveCriticalSection(&cs2);
LeaveCriticalSection(&cs1);
EnterCriticalSection(&cs2);
y++;
EnterCriticalSection(&cs1);
x++;
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
Поток №1 Поток №2
Нарушение порядка захвата
1. EnterCriticalSection(&cs1); в потоке №1
2. EnterCriticalSection(&cs2); в потоке №1
3. EnterCriticalSection(&cs2); в потоке №2
4. EnterCriticalSection(&cs1); в потоке №2
Взаимная блокировка
1. EnterCriticalSection(&cs1); в потоке №1
2. EnterCriticalSection(&cs2); в потоке №2
13
Активная взаимная блокировка
http://stackoverflow.com/a/27997039/2882509
Ложное разделение ресурсовнеумышленное разделение данных в кэш-линии
Условия:
Два или более потока «работают» с одной и той же кэш-линией, но с разными адресами в памяти.
Хотя бы один поток пишет в свой адрес памяти, фактически помечая кэш-линию на других ядрах как устаревшую.
Последствия:
При следующем обращении по своему адресу ядру будет необходимо обратиться к памяти снова, чтобы загрузить «свежие» данные из неё.
14
float a[N], b[N];
float localSum[NUM_PROCS];
void* work(int tid) {
for (int j = 0; j < ITERATIONS; j++) {
for (int i = tid; i < N; i+= NUM_PROCS) {
a[i] = i + a[i] * b[i];
localSum[tid] += a[i];
}
}
}
0 1 2 3 4 … N-1a[i]
0 1 2 3 4 … N-1b[i]
thread 1 thread 2
0 1 2 3 4 … 15cacheline
Ложное разделение ресурсов (пример)
15
Пусть NUM_PROCS = 2:
void* work(int tid) {
for (int j = 0; j < ITERATIONS; j++) {
for (int i = ;
i < ; i++)
{
a[i] = i + a[i] * b[i];
}
}
}
Ложное разделение ресурсов (пример)
16
int chunks = N / NUM_PROCS;
tid * chunks
(tid + 1) * chunks
float sum = 0.0f;
localSum[tid] += a[i];
localSum[tid] = sum;
sum += a[i];
Шаблоны параллельных алгоритмов
Шаблоны параллельных алгоритмов
18
• Fork-join запускает исполнение нескольких задач одновременно и затем дожидается завершения каждой из них• Удобен в применении для
функциональной и рекурсивной декомпозиции• Используется как базовый
блок для построения других шаблонов
Примеры: Сортировка слиянием, быстрая сортировка (Хоара), другие алгоритмы «разделяй-и-властвуй»
Шаблон: Fork-Join
19
Функциональная декомпозиция
20
int e;
main () {
int x[10], j, k, m; j = f(x, k); m = g(x, k);
...
}
int f(int *x, int k)
{
int a; a = e * x[k] * x[k]; return a;
}
int g(int *x, int k)
{
int a; k = k-1; a = e / x[k]; return a;
}
Поток №0
Поток №1
Статическая переменная: общаяГлобальная для потоков: общая
Локальные переменные функций
Рекурсивный (вложенный) параллелизм
21
Эффективная рекурсия с fork-join
•Легко «вкладывается»
•Накладные расходы делятся между потоками
Рекурсивный fork-join обеспечивает высокую степень параллелизма
22
• Map применяет указанную функцию к каждому элементу из заданного набора• Это может быть некий
набор данных или абстрактный индекс
• В серийной программе это частный случай итерирования –независимые операции.
A = map(f)(B);
Примеры: цветовая коррекция изображений; преобразование координат; трассировка лучей; методы Монте-Карло
Шаблон: Map
23
Параллелизм по данным
Последовательный код
24
for (int i = N/2; i < N; i++) a[i] = foo(i);
for (int i = 0; i < N/2; i++) a[i] = foo(i);
const int N = 1000;int a[N];for (int i = 0; i < N; i++) a[i] = foo(i);
Локальная памятьРазделяемая память
Поток №0
Поток №1
Параллелизм по данным
25
Разделяемая
память
Поток №0 Поток №2
Поток №1
f ( )
f ( )
f ( )
• Reduce объединяет, при помощи ассоциативной операции, все элементы набора в один элемент
• Например, reduce можно использовать, чтобы найти сумму элементов или максимальный элемент
b = reduce(f)(B);
Примеры: вычисление агрегатных функций; операции с матрицами; численное интегрирование
Шаблон: Reduce
26
• Конвейер – цепочка из стадий обработки потока данных
• Некоторые стадии могут иметь состояние
• Можно обрабатывать данные по мере поступления: “online”
Примеры: сжатие/распаковка данных, обработка сигналов, фильтрация изображений
Шаблон: Pipeline
27
Конвейер
Разные данные на разных стадиях
Разные данные в одной стадии, если там нет состояния
Данные на выходе могут быть переупорядочены
Может понадобиться буферизация между стадиями
28
Bzip2: схема конвейера
Read block
Run-length encoding
Output stream
checksum
bit position
output file ptr
Burrows-Wheeler Transform
Move To Front
Run-length Encoding
Huffman Coding
Checksum
Bit-align
Write block
Input stream
input file ptr
29
• Scan полезен в сценариях, когда данные по сути зависимы.
• Для достижения параллелизма необходимо слегка «извернуться».
• Необходимо, чтобы операция удовлетворяла правилу ассоциативности.
Примеры: вычисление частичных сумм, сортировка подсчётом, ранжирование списка, интегральные изображения
Шаблон: Scan
30
31
int n = 16; int temp = 0;
for(int i = 0; i < n; ++i) {
y[i] = temp;
temp = temp + z[i];
}
y[n] = temp;
Задача: найти частичные суммы
1 2 3 4 5 166 7 8 9 10 11 12 13 14 15z[]
y[] 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105120136
Реализация параллельныхпаттернов: OpenMP
Алексей Федотов, Михаил Дворский
SSG/DPD/TCAR/Threading Runtimes
22.02.2017
33
OpenMP «в двух словах»
• Стандарт де-факто, OpenMP 4.0, с июля 2013 года
• API для C/C++ и Fortran для модели параллельногопрограммирования с разделяемой моделью памяти
• Базируется на директивах (pragmas в C/C++)
• Портируемость (есть для многих платформ)
• Поддержка различных типов параллелизма
34
Явное выражение параллелизма
• Необходимо знание системного API.
• Дизайн и отладка задачи «выливаются» в программирование системных потоков.
Создание потоков…
Разбиение на подзадачи...
Синхронизация/ожидание...
Освобождение ресурсов...
35
Явное выражение параллелизма (Win32)for(int i=0; i < nThread; ++i) // создание потоков
hThreads[i] = CreateThread(NULL, 0, ThreadFn, threadData, CREATE_SUSPENDED, NULL);
ParallelFor(); // параллельная работа
for(int i=0; i < nThread; ++i) // освобождение ресурсов
CloseHandle(hThreads[i]);
DWORD ThreadFn(void* passedInData) {
ThreadData *pData = (ThreadData *)passedInData;
for(int i = pData->begin; i < pData->end; ++i )
pData->x[i] = (pData->y[i-1] + pData->y[i+1]) / 2;
return 0;
}
void ParallelFor() {
for(int i=0; i < nThread; ++i) // Запуск потоков
ResumeThread(hThreads[i]);
WaitForMultipleObjects(nThread, hThreads, TRUE, INFINITE);
}
36
Модель
Fork-join parallelism:
• Основной поток создаёт потоки по мере необходимости
• Параллелизм появляется в процессе исполнения: последовательная программа эволюционирует в параллельную
Параллельные регионы
Основной поток
37
Конструкции OpenMP
OpenMP поддерживает следующие директивы:
parallel, for, parallel for, section, sections, single, master, critical, flush, ordered и atomic
#pragma omp <директива> [клауза [ [,] клауза]...]
Самая важная директива - parallel. Она создает параллельный регион для следующего за ней структурированного блока
#pragma omp parallel [клауза[ [,] клауза]...]
структурированный блок
Для реализации параллельного выполнения блоков приложения нужно просто добавить в код директивы pragma
38
Параллельные регионы
Определяет параллельный регион над структурированным участком кода
Потоки создаются в тот месте кода, где стоит указание «parallel»
Потоки блокируются в конце региона
Данные становятся общими, если не специфицировано иное
#pragma omp parallel
Thread
1Thread
2Thread
3
C/C++ : #pragma omp parallel
{
block
}
39
Конструкция «Распределение работы»
Потокам назначаются независимые итерации
Потоки ждут в конце
#pragma omp parallel
#pragma omp for
Implicit barrier
i = 0
i = 1
i = 2
i = 3
i = 4
i = 5
i = 6
i = 7
i = 8
i = 9
i = 10
i = 11
#pragma omp parallel
#pragma omp for
for(i = 0; i < 12; i++)
c[i] = a[i] + b[i]
40
Комбинирование директив
Следующие сегменты кода эквивалентны
#pragma omp parallel
{
#pragma omp for
for (i=0; i< MAX; i++) {
res[i] = huge();
}
}
#pragma omp parallel for
for (i=0; i< MAX; i++) {
res[i] = huge();
}
41
Частные и общие и данные.
private. Порождает отдельную копию переменной для каждого потока. Переменные не инициализируются. С++ объекты конструируются по умолчанию. Значение переменных после параллельного региона неопределено.
firstprivate. Перед выполнением параллельного региона значение переменной основного потока копируется в каждый поток. Для С++ объектов используется конструктор копии.
lastprivate. При выполнении последней итерации параллельного цикла значение переменной, указанной в разделе lastprivate, присваивается переменной основного потока. Для С++ объектов используется оператор присваивания.
shared. Переменные «разделяются» между всеми потоками в параллельной секциии.
42
Частные и общие данные. Примеры
float dot_prod(float* a, float* b, int N)
{
float sum = 0.0;
#pragma omp parallel for shared(sum)
for(int i=0; i<N; i++) {
sum += a[i] * b[i];
}
return sum;
}
Всё ли верно?
void* work(float* c, int N) {
float x, y; int i;
#pragma omp parallel for private(x,y)
for(i=0; i<N; i++) {
x = a[i]; y = b[i];
c[i] = x + y;
}
}
43
Директива critical
Ответственность за предоставление эксклюзивного доступа к разделяемой памяти лежит на пользователе:
float dot_prod(float* a, float* b, int N)
{
float sum = 0.0;
#pragma omp parallel for shared(sum)
for(int i=0; i<N; i++) {
#pragma omp critical
sum += a[i] * b[i];
}
return sum;
}
44
Директива atomic
• Применимо только для простых операций записи
• Вычисление левой и правой частей оператора “+=“ не атомарные (!)
float dot_prod(float* a, float* b, int N)
{
float sum = 0.0;
#pragma omp parallel for shared(sum)
for(int i=0; i<N; i++) {
#pragma omp atomic
sum += a[i] * b[i];
}
return sum;
}
45
Директива reduction
Локальная копия переменной sum у каждого потока
Локальные копии складываются вместе, и сохраняются в глобальной переменной
#pragma omp parallel for reduction(+:sum)
for(i=0; i<N; i++) {
sum += a[i] * b[i];
}
46
Целый набор ассоциативных и коммутативных операций может быть использован с директивой редукции
Начальные значения те, которые имею смысл.
Oперации редукции (OpenMP 2.0)
Оператор Начальное значение
+ 0
* 1
- 0
^ 0
Оператор Начальное значение
& ~0
| 0
&& 1
|| 0
47
Директива schedule
Когда следует использовать
staticПредсказуемое и схожее количество работы на каждой итерации
dynamicНепредсказуемое, высоко изменчивое количество работы на каждой итерации
guidedСпециальный случай динамического распределения. Призван сократить накладные расходы, связанные с самим распределением
auto Решение о распределении принимается компилятором
runtimeРаспределение задаётся переменной окружения OMP_SCHEDULE.
Распределение работы в цикле
48
Пример использования директивы schedule
#pragma omp parallel for schedule (static, 8)
for( int i = start; i <= end; i += 2 )
{
if ( TestForPrime(i) ) foo(i);
}
• Итерации разделяются блоками по 8
• Если start = 3, то первый блок включает итерации 3, 5, 7, 9,11, 13, 15, 17
49
Параллельные секции
Независимые секции кода могут исполняться одновременно
#pragma omp parallel sections
{
#pragma omp section
phase1();
#pragma omp section
phase2();
#pragma omp section
phase3();
}
50
Обозначает блок кода, который исполнится только одним потоком
При этом не специфицируется каким именно
Неявный барьер в конце
Директива Single
#pragma omp parallel
{
DoManyThings();
#pragma omp single
{
ExchangeBoundaries();
} // threads wait here for single
DoManyMoreThings();
}
51
Обозначает блок кода, который исполнится только основным потоком
Нет неявного барьера в конце
Директива Master
#pragma omp parallel
{
DoManyThings();
#pragma omp master
{ // if not master skip to next stmt
ExchangeBoundaries();
}
DoManyMoreThings();
}
52
Неявные барьеры
Несколько OpenMP директив имеют неявные барьеры
parallel
for
single
sections
Излишние барьеры отрицательно сказываются на производительности
Ожидающие потоки не делают полезной работы!
Можно указать OpenMP не использовать неявные барьерыпри помощи клаузы nowait
53
Клауза “nowait”
Используйте, когда потокам не требуется ожидания, например, между независимыми вычислениями
#pragma single nowait
{ [...] }
#pragma omp for nowait
for(...)
{...};
#pragma omp for schedule(dynamic,1) nowait
for(int i=0; i<n; i++)
a[i] = bigFunc1(i);
#pragma omp for schedule(dynamic,1)
for(int j=0; j<m; j++)
b[j] = bigFunc2(j);
54
Директива “barrier”
Позволяет задать синхронизацию явно
Каждый поток ждёт пока «не придут» остальные
#pragma omp parallel shared (A, B, C)
{
DoSomeWork(A,B);
printf(“Processed A into B\n”);
#pragma omp barrier
DoSomeWork(B,C);
printf(“Processed B into C\n”);
}
55
Уровни параллелизма
Task Parallelism / MessagPassing
forkjoin
SIMD SIMD SIMD
fork-join
SIMD SIMD
Fork-join
SIMD SIMD SIMD
OpenMP 2.0
#pragma omp for
OpenMP 4.0
#pragma omp simd
56
Example (omp simd)float dot_product(float* a, float* b, int n) {
float sum = 0.0f;
#pragma omp simd reduction(+:sum)
for(int i=0; i<n; ++i)
sum += a[i] * b[i];
return sum;
}
4x floats
OpenMP 3.0/4.0 (Основные моменты)
• Поддержка задач
• Поддержка акселераторов с помощью target директив
• Поддержка SIMD директив для векторизации
• Поддержка зависимости между задачами (одного уровня), приоритеты задач
• Поддержка пользовательской редукции
• Явная «привязка» потоков к «железу»
• Обработка ошибок
• Поддержка Fortran 2003
57