Лекция 8. Intel Threading Building Blocks
- 1. КурносовМихаил Георгиевич
E-mail: mkurnosov@gmail.com
WWW: www.mkurnosov.net
Курс “Высокопроизводительные вычислительные системы”
Сибирский государственный университет телекоммуникаций и информатики (Новосибирск)
Осенний семестр, 2014
Лекция 8Intel Threading Building Blocks
- 2. Intel Threading Building Blocks
2
Intel Treading Building Blocks (TBB) – это кроссплатформенная библиотека шаблонов C++ для создания многопоточных программ
История развития:
o2006 –Intel TBB v1.0 (Intel compiler only)
o2007 –Intel TBB v2.0 (Open Source, GPLv2)
o2008–Intel TBB v2.1(thread affinity, cancellation)
o2009–Intel TBB v2.2(C++0x lambda functions)
o…
o2011–Intel TBB v4.0
o2012 –Intel TBB v4.1
o2014–Intel TBB v4.3
http://threadingbuildingblocks.org
- 3. Intel Threading Building Blocks
3
Open Source Community Version GPL v2
Поддерживаемые операционные системы:
oMicrosoft Windows {XP, 7, Server 2008, …}
oGNU/Linux + Android
oApple Mac OSX 10.7.4, …
http://threadingbuildingblocks.org
- 4. Состав Intel TBB
4
Алгоритмы:parallel_for,parallel_reduce, parallel_scan, parallel_while,parallel_do, parallel_pipeline,parallel_sort
Контейнеры:concurrent_queue, concurrent_vector,concurrent_hash_map
Аллокаторыпамяти:scalable_malloc,scalable_free, scalable_realloc,scalable_calloc, scalable_allocator,cache_aligned_allocator
Мьютексы: mutex,spin_mutex,queuing_mutex, spin_rw_mutex,queuing_rw_mutex,recursive mutex
Атомарные операции: fetch_and_add, fetch_and_increment,fetch_and_decrement, compare_and_swap,fetch_and_store
Task-based parallelism (fork-join) + work stealing
- 5. Intel Threading Building Blocks
5
Intel TBB позволяет абстрагироваться от низкоуровневых потоков и распараллеливать программу в терминах параллельно выполняющихся задач(task parallelism)
Задачи TBB “легче” потоков операционной системы
Планировщик TBB использует механизм “work stealing” для распределения задач по потокам
Все компоненты Intel TBB определены в пространстве имен C++ (namespace)“tbb”
- 6. Компиляция программ с Intel TBB
6
$ g++ –Wall –o prog./prog.cpp –ltbb
C:> icl/MD prog.cpp tbb.lib
GNU/Linux
Microsoft Windows (Intel C++ Compiler)
- 7. //
// tbb_hello.cpp: TBB Hello World
//
#include <cstdio>
#include <tbb/tbb.h>
// Function object
classMyTask{
public:
MyTask(constchar*name): name_(name) {}
voidoperator()() const
{
// Task code
std::printf("Hello from task %sn", name_);
}
private:
constchar *name_;
};
Intel TBB: Hello World!
- 8. Intel TBB: Hello World! (продолжение)
8
intmain( )
{
tbb::task_grouptg;
tg.run(MyTask("1")); // Spawn task
tg.run(MyTask("2")); // Spawn task
tg.wait(); // Wait tasks
return0;
}
- 9. Компиляция и запускtbb_hello
9
$ g++ -Wall -I~/opt/tbb/include
-L~/opt/tbb/lib
-o tbb_hello
./tbb_hello.cpp-ltbb
$ ./tbb_hello
Hello from task 2
Hello from task 1
- 10. Инициализация библиотеки
10
Любой поток использующий алгоритмыили планировщик TBB должен иметь инициализированный объект tbb::task_scheduler_init
TBB >= 2.2 автоматически инициализирует планировщик
Явная инициализация планировщика позволяет:
управлять когда создается и уничтожается планировщик
устанавливать количество используемых потоков выполнения
устанавливать размер стека для потоков выполнения
- 11. Инициализация библиотеки
11
#include <tbb/task_scheduler_init.h>
intmain()
{
tbb::task_scheduler_initinit;
return0;
}
Явная инициализация планировщика
- 12. Инициализация библиотеки
12
Конструктор класса task_scheduler_initпринимает два параметра:
task_scheduler_init(intmax_threads=automatic, stack_size_typethread_stack_size=0);
Допустимые значения параметра max_threads:
task_scheduler_init::automatic – количество потоков определяется автоматически
task_scheduler_init::deferred– инициализация откладывается до явного вызова метода task_scheduler_init::initialize(max_threads)
Положительное целое–количество потоков
- 13. Инициализация библиотеки
13
#include <iostream>
#include <tbb/task_scheduler_init.h>
intmain()
{
intn = tbb::task_scheduler_init::default_num_threads();
for(intp = 1; p <= n; ++p) {
// Construct task scheduler with p threads
tbb::task_scheduler_initinit(p);
std::cout<< "Is active = "<< init.is_active()
<< std::endl;
}
return0;
}
- 14. Распараллеливание циклов
14
В TBB реализованы шаблоны параллельных алгоритмов
parallel_for
parallel_reduce
parallel_scan
parallel_do
parallel_for_each
parallel_pipeline
parallel_sort
parallel_invoke
- 15. parallel_for
15
voidsaxpy(floata, float*x, float*y, size_tn)
{
for(size_ti= 0; i< n; ++i)
y[i] += a * x[i];
}
parallel_forпозволяет разбить пространство итерации на бло��и (chunks), которые обрабатываются разными потоками
Требуется создать класс, в котором перегруженный оператор вызова функции operator()содержит код обработки блока итераций
- 16. #include <iostream>
#include <tbb/task_scheduler_init.h>
#include <tbb/tick_count.h>
#include <tbb/parallel_for.h>
#include <tbb/blocked_range.h>
classsaxpy_par{
public:
saxpy_par(floata, float*x, float*y):
a_(a), x_(x), y_(y) {}
voidoperator()(constblocked_range<size_t> &r) const
{
for(size_ti= r.begin(); i!= r.end(); ++i)
y_[i] += a_ * x_[i];
}
private:
floatconsta_;
float*constx_;
float*consty_;
};
parallel_for
16
- 17. intmain()
{
floata = 2.0;
float*x, *y;
size_tn = 100000000;
x = newfloat[n];
y = newfloat[n];
for(size_ti= 0; i< n; ++i)
x[i] = 5.0;
tick_countt0 = tick_count::now();
task_scheduler_initinit(4);
parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y),
auto_partitioner());
tick_countt1 = tick_count::now();
cout<< "Time: "<< (t1 -t0).seconds() << " sec."<< endl;
delete[] x;
delete[] y;
return0;
}
parallel_for
17
- 18. intmain()
{
floata = 2.0;
float*x, *y;
size_tn = 100000000;
x = newfloat[n];
y = newfloat[n];
for(size_ti= 0; i< n; ++i)
x[i] = 5.0;
tick_countt0 = tick_count::now();
task_scheduler_initinit(4);
parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y),
auto_partitioner());
tick_countt1 = tick_count::now();
cout<< "Time: "<< (t1 -t0).seconds() << " sec."<< endl;
delete[] x;
delete[] y;
return0;
}
parallel_for
18
Классblocked_range(begin, end, grainsize) описывает одномерное пространство итераций
В Intel TBB доступно описание многомерных пространств итераций (blocked_range2d, ...)
- 19. affinity_partitioner
19
Класс affinity_partitionerзапоминает какими потоками выполнялись предыдущие итерации и пытается распределять блоки итераций с учетом этой информации –последовательные блоки ��азначаются на один и тот же поток для эффективного использования кеш-памяти
intmain()
{
// ...
static affinity_partitionerap;
parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y), ap);
// ...
return 0;
}
- 20. intmain()
{
// ...
x = newfloat[n];
y = newfloat[n];
for(size_ti= 0; i< n; ++i)
x[i] = 5.0;
tick_countt0 = tick_count::now();
parallel_for(blocked_range<size_t>(0, n),
[=](constblocked_range<size_t>& r) {
for (size_ti= r.begin(); i!= r.end(); ++i)
y[i] += a * x[i];
}
);
tick_countt1 = tick_count::now();
cout<< "Time: "<< (t1 -t0).seconds() << " sec."<< endl;
delete[] x;
delete[] y;
return0;
}
parallel_for(C++11 lambda expressions)
20
- 21. intmain()
{
// ...
x = newfloat[n];
y = newfloat[n];
for(size_ti= 0; i< n; ++i)
x[i] = 5.0;
tick_countt0 = tick_count::now();
parallel_for(blocked_range<size_t>(0, n),
[=](constblocked_range<size_t>& r) {
for (size_ti= r.begin(); i!= r.end(); ++i)
y[i] += a * x[i];
}
);
tick_countt1 = tick_count::now();
cout<< "Time: "<< (t1 -t0).seconds() << " sec."<< endl;
delete[] x;
delete[] y;
return0;
}
parallel_for(C++11 lambda expressions)
21
Анонимная функция (лямбда-функция, С++11)
[=]–захватить все автоматические переменные
(constblocked_range…) –аргументы функции
{ ... } –код функции
- 22. parallel_reduce
22
floatreduce(float*x, size_tn)
{
floatsum = 0.0;
for(size_ti= 0; i< n; ++i)
sum += x[i];
returnsum;
}
parallel_reduceпозволяет распараллеливать циклы и выполнять операцию редукции
- 23. classreduce_par{
public:
floatsum;
void operator()(constblocked_range<size_t> &r)
{
floatsum_local= sum;
float*xloc= x_;
size_tend = r.end();
for(size_ti= r.begin(); i!= end; ++i)
sum_local+= xloc[i];
sum = sum_local;
}
// Splitting constructor: вызывается при порождении новой задачи
reduce_par(reduce_par& r, split): sum(0.0), x_(r.x_) {}
// Join: объединяет результаты двух задач (текущей и r)
voidjoin(constreduce_par& r) {sum += r.sum;}
reduce_par(float*x): sum(0.0), x_(x) {}
private:
float*x_;
};
parallel_reduce
23
- 24. intmain()
{
size_tn = 10000000;
float*x = newfloat[n];
for(size_ti= 0; i< n; ++i)
x[i] = 1.0;
tick_countt0 = tick_count::now();
reduce_parr(x);
parallel_reduce(blocked_range<size_t>(0, n), r);
tick_countt1 = tick_count::now();
cout<< "Reduce: " << std::fixed << r.sum<< "n";
cout<< "Time: " << (t1 -t0).seconds() << " sec." << endl;
delete[] x;
return0;
}
parallel_reduce
24
- 25. parallel_sort
25
voidparallel_sort(RandomAccessIteratorbegin,
RandomAccessIteratorend,
constCompare& comp);
parallel_sortпозволяет упорядочивать последовательностиэлементов
Применяется детерминированный алгоритм нестабильной сортировки с трудоемкостью O(nlogn)–алгоритм не гарантирует сохранения порядка следования элементов с одинаковыми ключами
- 26. parallel_sort
26
#include <cstdlib>
#include <tbb/parallel_sort.h>
using namespace std;
using namespace tbb;
intmain()
{
size_tn = 10;
float*x = newfloat[n];
for(size_ti= 0; i< n; ++i)
x[i] = static_cast<float>(rand()) / RAND_MAX * 100;
parallel_sort(x, x + n, std::greater<float>());
delete[] x;
return 0;
}
- 27. Планировщик задач (Task scheduler)
27
Intel TBB позволяет абстрагироваться от реальных потоков операционной системы и разрабатывать программу в терминах параллельных задач (task-based parallel programming)
Запуск TBB-задачи примерно в 18 раз быстрее запуска потока POSIXв GNU/Linux (в Microsoft Windows примерно в 100 разбыстрее)
В отличии от планировщика POSIX-потоков в GNU/Linux планировщик TBBреализует “не справедливую” (unfair) политику распределения задач по потокам
- 29. intfib_par(intn)
{
intval;
fibtask& t = *new(task::allocate_root()) fibtask(n, &val);
task::spawn_root_and_wait(t);
returnval;
}
Числа Фибоначчи: parallel version
29
allocate_rootвыделяет память под корневую задачу(task)класса fibtask
spawn_root_and_waitзапускает задачу на выполнение и ожидает её завершения
- 30. classfibtask: publictask{
public:
constintn;
int* constval;
fibtask(intn_, int* val_): n(n_), val(val_) {}
task* execute()
{
if(n < 10) {
*val= fib(n); // Use sequential version
} else{
intx, y;
fibtask& a = *new(allocate_child()) fibtask(n -1, &x);
fibtask& b = *new(allocate_child()) fibtask(n -2, &y);
// ref_count: 2 children + 1 for the wait
set_ref_count(3);
spawn(b);
spawn_and_wait_for_all(a);
*val= x + y;
}
returnNULL;
}
};
Числа Фибоначчи: parallel version
30
spawnзапускает задачу на выполнение и не ожидает её завершения
spawn_and_wait_for_all–запускает задачу и ожидает завершения всех дочерних задач
- 31. intmain()
{
intn = 42;
tick_countt0 = tick_count::now();
intf = fib_par(n);
tick_countt1 = tick_count::now();
cout<< "Fib = " << f << endl;
cout<< "Time: " << std::fixed << (t1 -t0).seconds()
<< " sec." << endl;
return0;
}
Числа Фибоначчи: parallel version
31
- 32. Граф задачи (Task graph)
32
Task A
Depth = 0
Refcount= 2
Task B
Depth = 1
Refcount= 2
Task C
Depth = 2
Refcount= 0
Task D
Depth = 2
Refcount= 0
Task E
Depth = 1
Refcount= 0
- 33. Планирование задач (Task scheduling)
33
Каждый поток поддерживает дек готовых к выполнению задач (deque, двусторонняя очередь)
Планировщик использует комбинированный алгоритма на основе обход графа задач в ширину и глубину
Task E
Task D
Top: Oldest task
Bottom: Youngest Task
- 34. Планирование задач (Task scheduling)
34
Листовые узлы в графе задач –это задачи готовые к выполнению (ready task, они не ожидают других)
Потоки могу захватывать (steal) задачи из чужих деков (с их верхнего конца)
Task E
Task D
Top: Oldest task
Bottom: Youngest Task
Top: Oldest task
Bottom: Youngest Task
- 35. Выбор задачи из дека
35
Задача для выполнения выбирается одним из следующих способов (в порядке уменьшения приоритета):
1.Выбирается задача, на которую возвращен указатель методом executeпредыдущей задачи
2.Выбирается задача с нижнего конца (bottom) дека потока
3.Выбирается первая задача из дека (с его верхнего конца) случайно выбранного потока–work stealing
- 36. Помещение задачи в дек потока
36
Задачи помещаются в дек с его нижнего конца
В дек помещается задача порожденная методомspawn
Задача может быть направлена на повторное выполнение методом task::recycle_to_reexecute
Задача имеет счетчик ссылок (reference count) равный нулю –все дочерние задачи завершены
- 37. Потокобезопасныеконтейнеры
37
Intel TBB предоставляет классы контейнеров (concurrent containers), которые корректно могут обновляться из нескольких потоков
Для работы в многопоточной программе со стандартными контейнерами STL доступ к ним необходимо защищать блокировками (мьютексами)
Особенности Intel TBB:
oпри работе с контейнерами применяет алгоритмы не требующие блокировок (lock-free algorithms)
oпри необходимости блокируются лишь небольшие участки кода контейнеров (fine-grained locking)
- 40. Взаимные исключения (Mutual exclusion)
40
Взаимные исключения (mutual exclusion)позволяют управлять количеством по��оков, одновременно выполняющих заданный участок кода
В Intel TBB взаимные исключения реализованы средствами мьютексов(mutexes) и блокировок (locks)
Мьютекс(mutex)–это объект синхронизации, который в любой момент времени может быть захвачен только одним потоком, остальные потоки ожидают его освобождения
- 41. Свойства мьютексовIntel TBB
41
Scalable
Fair–справедливые мьютексызахватываются в порядке обращения к ним потоков (даже если следующий поток в очереди находится в состоянии сна; несправедливыемьютексымогут быть быстрее)
Recursive–рекурсивные мьютексыпозволяют потоку захватившему мьютексповторно его получить
Yield–при длительном ожидании мьютексапоток периодически проверяет его текущее состояние и снимает себя с процессора (засыпает, в GNU/Linux вызывается sched_yield(),а в Microsoft Windows –SwitchToThread())
Block–потока освобождает процессор до тех пор, пока не освободится мьютекс(такие мьютексырекомендуется использовать при длительных ожиданиях)
- 42. МьютексыIntel TBB
42
spin_mutex–поток ожидающий освобождения мьютексавыполняет пустой цикл ожидания (busy wait)
spin_mutexрекомендуется использовать для защиты небольших участков кода (нескольких инструкций)
queuing_mutex–scalable, fair, non-recursive, spins in user space
spin_rw_mutex–spin_mutex+ reader lock
mutexи recursive_mutex–это обертки вокруг взаимных исключений операционной системы (Microsoft Windows –CRITICAL_SECTION, GNU/Linux –мьютексыбиблиотеки pthread)
- 43. МьютексыIntel TBB
43
Mutex
Scalable
Fair
Recursive
LongWait
Size
mutex
OSdep.
OSdep.
No
Blocks
>= 3 words
recursive_mutex
OSdep.
OSdep.
Yes
Blocks
>= 3 words
spin_mutex
No
No
No
Yields
1 byte
queuing_mutex
Yes
Yes
No
Yields
1 word
spin_rw_mutex
No
No
No
Yields
1 word
queuing_rw_mutex
Yes
Yes
No
Yields
1 word
- 44. spin_mutex
44
ListNode*FreeList;
spin_mutexListMutex;
ListNode*AllocateNode()
{
ListNode*node;
{
// Создать и захватить мьютекс(RAII)
spin_mutex::scoped_locklock(ListMutex);
node = FreeList;
if(node)
FreeList= node->next;
}// Мьютексавтоматически освобождается
if(!node)
node = new ListNode()
returnnode;
}
- 45. spin_mutex
45
voidFreeNode(ListNode*node)
{
spin_mutex::scoped_locklock(ListMutex);
node->next = FreeList;
FreeList= node;
}
Конструктор scoped_lockожидает освобождения мьютексаListMutex
Структурный блок (операторные скобки {}) внутриAllocateNodeнужен для того, чтобы при выходе из него автоматически вызывался деструктор класса scoped_lock, который освобождает мьютекс
Программная идиома RAII –Resource Acquisition Is Initialization (получение ресурса есть инициализация)
- 46. spin_mutex
46
ListNode*AllocateNode()
{
ListNode*node;
spin_mutex::scoped_locklock;
lock.acquire(ListMutex);
node = FreeList;
if(node)
FreeList= node->next;
lock.release();
if(!node)
node = new ListNode();
returnnode;
}
Если защищенный блок (acquire-release) сгенерирует исключение, то release вызван не будет!
Используйте RAIIесли в пределах критической секции возможно возникновение исключительной ситуации
- 47. Атомарные операции (Atomic operations)
47
Атомарная операция(Atomic operation)–это операций, которая в любой момент времени выполняется только одним потоком
Атомарные операции намного “легче”мьютексов– не требуют блокирования потоков
TBB поддерживаем атомарные переменные
atomic<T> AtomicVariableName
- 48. Атомарные операции (Atomic operations)
48
Операции над переменной atomic<T> x
= x-чтение значения переменной x
x = -запись в переменную xзначения и его возврат
x.fetch_and_store(y) x= yи возврат старого значения x
x.fetch_and_add(y) x+= yи возврат старого значения x
x.compare_and_swap(y, z) если x= z, то x= y, возврат старого значения x
- 49. Атомарные операции (Atomic operations)
49
atomic<int> counter;
unsigned intGetUniqueInteger()
{
returncounter.fetch_and_add(1);
}
- 50. Атомарные операции (Atomic operations)
50
atomic<int> Val;
intUpdateValue()
{
do{
v = Val;
newv= f(v);
} while(Val.compare_and_swap(newv, v) != v);
returnv;
}
- 51. Аллокаторыпамяти
51
Intel TBB предоставляет два аллокаторапамяти (альтернативы STL std::allocator)
scalable_allocator<T>–обеспечивает параллельное выделение памяти нескольким потокам
cache_aligned_allocator<T>–обеспечивает выделение блоков памяти, выравненных на границудлины кеш- линии (cacheline)
Это позволяет избежать ситуации когда потоки на разных процессорах пытаются модифицировать разные слова памяти, попадающие в одну строку кэша, и как следствие, постоянно перезаписываемую из памяти в кеш
- 52. Аллокаторыпамяти
52
/* STL vector будет использовать аллокаторTBB */
std::vector<int, cache_aligned_allocator<int> > v;
- 53. Ссылки
53
James Reinders. Intel Threading Building Blocks. –O'Reilly, 2007. –336p.
Intel Threading Building Blocks Documentation// http://software.intel.com/sites/products/documentation/doclib/tbb_sa/help/index.htm