Лекция 8. intel threading building blocks
DESCRIPTION
Intel Threading Building BlocksTRANSCRIPT
Курносов Михаил Георгиевич
E-mail: [email protected]: www.mkurnosov.net
Курс “Высокопроизводительные вычислительные системы”Сибирский государственный университет телекоммуникаций и информатики (Новосибирск)Осенний семестр, 2014
Лекция 8Intel Threading Building Blocks
Intel Threading Building Blocks
22
Intel Treading Building Blocks (TBB) –
это кроссплатформенная библиотека шаблонов
C++ для создания многопоточных программ
История развития:
o 2006 – Intel TBB v1.0 (Intel compiler only)
o 2007 – Intel TBB v2.0 (Open Source, GPLv2)
o 2008 – Intel TBB v2.1 (thread affinity, cancellation)
o 2009 – Intel TBB v2.2 (C++0x lambda functions)
o …
o 2011 – Intel TBB v4.0
o 2012 – Intel TBB v4.1
o 2014 – Intel TBB v4.3
http://threadingbuildingblocks.org
Intel Threading Building Blocks
33
Open Source Community Version GPL v2
Поддерживаемые операционные системы:
o Microsoft Windows {XP, 7, Server 2008, …}
o GNU/Linux + Android
o Apple Mac OS X 10.7.4, …
http://threadingbuildingblocks.org
Состав Intel TBB
44
Алгоритмы: 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
Intel Threading Building Blocks
55
Intel TBB позволяет абстрагироваться от низкоуровневых
потоков и распараллеливать программу в терминах
параллельно выполняющихся задач (task parallelism)
Задачи TBB “легче” потоков операционной системы
Планировщик TBB использует механизм “work stealing”
для распределения задач по потокам
Все компоненты Intel TBB определены в пространстве
имен C++ (namespace) “tbb”
Компиляция программ с Intel TBB
66
$ g++ –Wall –o prog ./prog.cpp –ltbb
C:\> icl /MD prog.cpp tbb.lib
GNU/Linux
Microsoft Windows (Intel C++ Compiler)
// // tbb_hello.cpp: TBB Hello World//#include <cstdio>#include <tbb/tbb.h>
// Function object class MyTask {public:
MyTask(const char *name): name_(name) {}
void operator()() const{
// Task codestd::printf("Hello from task %s\n", name_);
}
private:const char *name_;
};
Intel TBB: Hello World!
Intel TBB: Hello World! (продолжение)
88
int main( ){
tbb::task_group tg;
tg.run(MyTask("1")); // Spawn tasktg.run(MyTask("2")); // Spawn tasktg.wait(); // Wait tasks
return 0;}
Компиляция и запуск tbb_hello
99
$ 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
Инициализация библиотеки
1010
Любой поток использующий алгоритмы или планировщик TBB должен иметь инициализированный объект tbb::task_scheduler_init
TBB >= 2.2 автоматически инициализирует планировщик
Явная инициализация планировщика позволяет:
управлять когда создается и уничтожается планировщик
устанавливать количество используемых потоков выполнения
устанавливать размер стека для потоков выполнения
Инициализация библиотеки
1111
#include <tbb/task_scheduler_init.h>
int main() {
tbb::task_scheduler_init init;
return 0;}
Явная инициализация планировщика
Инициализация библиотеки
1212
Конструктор класса task_scheduler_init принимает
два параметра:
task_scheduler_init(int max_threads = automatic,
stack_size_type thread_stack_size = 0);
Допустимые значения параметра max_threads:
task_scheduler_init::automatic –количество потоков определяется автоматически
task_scheduler_init::deferred –инициализация откладывается до явного вызова метода task_scheduler_init::initialize(max_threads)
Положительное целое – количество потоков
Инициализация библиотеки
1313
#include <iostream>
#include <tbb/task_scheduler_init.h>
int main()
{
int n = tbb::task_scheduler_init::default_num_threads();
for (int p = 1; p <= n; ++p) {
// Construct task scheduler with p threads
tbb::task_scheduler_init init(p);
std::cout << "Is active = " << init.is_active()
<< std::endl;
}
return 0;
}
Распараллеливание циклов
1414
В TBB реализованы шаблоны параллельных алгоритмов
parallel_for
parallel_reduce
parallel_scan
parallel_do
parallel_for_each
parallel_pipeline
parallel_sort
parallel_invoke
parallel_for
1515
void saxpy(float a, float *x, float *y, size_t n)
{
for (size_t i = 0; i < n; ++i)
y[i] += a * x[i];
}
parallel_for позволяет разбить пространство итерации
на блоки (chunks), которые обрабатываются разными
потоками
Требуется создать класс, в котором перегруженный
оператор вызова функции operator() содержит код
обработки блока итераций
#include <iostream>#include <tbb/task_scheduler_init.h>#include <tbb/tick_count.h>#include <tbb/parallel_for.h>#include <tbb/blocked_range.h>
class saxpy_par {public:
saxpy_par(float a, float *x, float *y):a_(a), x_(x), y_(y) {}
void operator()(const blocked_range<size_t> &r) const{
for (size_t i = r.begin(); i != r.end(); ++i) y_[i] += a_ * x_[i];
}
private:float const a_;float *const x_;float *const y_;
};
parallel_for
1616
int main() {
float a = 2.0;float *x, *y;size_t n = 100000000;
x = new float[n];y = new float[n];for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();task_scheduler_init init(4);parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y),
auto_partitioner());tick_count t1 = tick_count::now();cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;delete[] y;
return 0;}
parallel_for
1717
int main() {
float a = 2.0;float *x, *y;size_t n = 100000000;
x = new float[n];y = new float[n];for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();task_scheduler_init init(4);parallel_for(blocked_range<size_t>(0, n), saxpy_par(a, x, y),
auto_partitioner());tick_count t1 = tick_count::now();cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;delete[] y;
return 0;}
parallel_for
1818
Класс blocked_range(begin, end, grainsize) описывает одномерное
пространство итераций
В Intel TBB доступно описание многомерных пространств итераций
(blocked_range2d, ...)
affinity_partitioner
1919
Класс affinity_partitioner запоминает какими потоками выполнялись предыдущие итерации и пытается распределять блоки итераций с учетом этой информации – последовательные блоки назначаются на один и тот же поток для эффективного использования кеш-памяти
int main(){
// ... static affinity_partitioner ap;parallel_for(blocked_range<size_t>(0, n),
saxpy_par(a, x, y), ap);// ...return 0;
}
int main() {
// ...x = new float[n];y = new float[n];for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();parallel_for(blocked_range<size_t>(0, n),
[=](const blocked_range<size_t>& r) {for (size_t i = r.begin(); i != r.end(); ++i)
y[i] += a * x[i];
});
tick_count t1 = tick_count::now();cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;delete[] y; return 0;
}
parallel_for (C++11 lambda expressions)
2020
int main() {
// ...x = new float[n];y = new float[n];for (size_t i = 0; i < n; ++i)
x[i] = 5.0;
tick_count t0 = tick_count::now();parallel_for(blocked_range<size_t>(0, n),
[=](const blocked_range<size_t>& r) {for (size_t i = r.begin(); i != r.end(); ++i)
y[i] += a * x[i];
});
tick_count t1 = tick_count::now();cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;delete[] y; return 0;
}
parallel_for (C++11 lambda expressions)
2121
Анонимная функция (лямбда-функция, С++11) [=] – захватить все автоматические переменные (const blocked_range …) – аргументы функции { ... } – код функции
parallel_reduce
2222
float reduce(float *x, size_t n){
float sum = 0.0;for (size_t i = 0; i < n; ++i)
sum += x[i];return sum;
}
parallel_reduce позволяет распараллеливать циклы
и выполнять операцию редукции
class reduce_par {public:
float sum;
void operator()(const blocked_range<size_t> &r){
float sum_local = sum;float *xloc = x_;size_t end = r.end();for (size_t i = 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)void join(const reduce_par& r) {sum += r.sum;}
reduce_par(float *x): sum(0.0), x_(x) {}
private:float *x_;
};
parallel_reduce
2323
int main()
{
size_t n = 10000000;
float *x = new float[n];
for (size_t i = 0; i < n; ++i)
x[i] = 1.0;
tick_count t0 = tick_count::now();
reduce_par r(x);
parallel_reduce(blocked_range<size_t>(0, n), r);
tick_count t1 = tick_count::now();
cout << "Reduce: " << std::fixed << r.sum << "\n";
cout << "Time: " << (t1 - t0).seconds() << " sec." << endl;
delete[] x;
return 0;
}
parallel_reduce
2424
parallel_sort
2525
void parallel_sort(RandomAccessIterator begin,
RandomAccessIterator end,
const Compare& comp);
parallel_sort позволяет упорядочивать последовательности
элементов
Применяется детерминированный алгоритм
нестабильной сортировки с трудоемкостью O(nlogn) – алгоритм
не гарантирует сохранения порядка следования элементов с
одинаковыми ключами
parallel_sort
2626
#include <cstdlib>#include <tbb/parallel_sort.h>
using namespace std;using namespace tbb;
int main() {
size_t n = 10;float *x = new float[n];for (size_t i = 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;
}
Планировщик задач (Task scheduler)
2727
Intel TBB позволяет абстрагироваться от реальных потоков
операционной системы и разрабатывать программу в
терминах параллельных задач (task-based parallel
programming)
Запуск TBB-задачи примерно в 18 раз быстрее запуска
потока POSIX в GNU/Linux (в Microsoft Windows примерно
в 100 раз быстрее)
В отличии от планировщика POSIX-потоков в GNU/Linux
планировщик TBB реализует “не справедливую” (unfair)
политику распределения задач по потокам
Числа Фибоначчи: sequential version
2828
int fib(int n){
if (n < 2)return n;
return fib(n - 1) + fib(n - 2); }
int fib_par(int n){
int val;
fibtask& t = *new(task::allocate_root()) fibtask(n, &val);task::spawn_root_and_wait(t);
return val;}
Числа Фибоначчи: parallel version
2929
allocate_root выделяет память под корневую задачу (task) класса fibtask
spawn_root_and_wait запускает задачу на выполнение и ожидает её
завершения
class fibtask: public task {public:
const int n;int* const val;
fibtask(int n_, int* val_): n(n_), val(val_) {}
task* execute() {
if (n < 10) {*val = fib(n); // Use sequential version
} else {int x, 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 waitset_ref_count(3);spawn(b);spawn_and_wait_for_all(a);*val = x + y;
}return NULL;
}};
Числа Фибоначчи: parallel version
3030
spawn запускает задачу на выполнение и не ожидает её завершения
spawn_and_wait_for_all – запускает задачу и ожидает завершения всех дочерних задач
int main() {
int n = 42;
tick_count t0 = tick_count::now();int f = fib_par(n);tick_count t1 = tick_count::now();
cout << "Fib = " << f << endl;cout << "Time: " << std::fixed << (t1 - t0).seconds()
<< " sec." << endl;return 0;
}
Числа Фибоначчи: parallel version
3131
Граф задачи (Task graph)
3232
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
Планирование задач (Task scheduling)
3333
Каждый поток поддерживает дек готовых к выполнению
задач (deque, двусторонняя очередь)
Планировщик использует комбинированный алгоритма
на основе обход графа задач в ширину и глубину
Task E
Task D
Top:Oldest
task
Bottom: Youngest
Task
Планирование задач (Task scheduling)
3434
Листовые узлы в графе задач – это задачи готовые
к выполнению (ready task, они не ожидают других)
Потоки могу захватывать (steal) задачи из чужих деков
(с их верхнего конца)
Task E
Task D
Top:Oldest
task
Bottom: Youngest
Task
Top:Oldest
task
Bottom: Youngest
Task
Выбор задачи из дека
3535
Задача для выполнения выбирается одним из следующих
способов (в порядке уменьшения приоритета):
1. Выбирается задача, на которую возвращен указатель
методом execute предыдущей задачи
2. Выбирается задача с нижнего конца (bottom) дека потока
3. Выбирается первая задача из дека (с его верхнего конца)
случайно выбранного потока – work stealing
Помещение задачи в дек потока
3636
Задачи помещаются в дек с его нижнего конца
В дек помещается задача порожденная методом spawn
Задача может быть направлена на повторное
выполнение методом
task::recycle_to_reexecute
Задача имеет счетчик ссылок (reference count)
равный нулю – все дочерние задачи завершены
Потокобезопасные контейнеры
3737
Intel TBB предоставляет классы контейнеров
(concurrent containers), которые корректно могут
обновляться из нескольких потоков
Для работы в многопоточной программе со
стандартными контейнерами STL доступ к ним
необходимо защищать блокировками (мьютексами)
Особенности Intel TBB:
o при работе с контейнерами применяет алгоритмы
не требующие блокировок (lock-free algorithms)
o при необходимости блокируются лишь небольшие
участки кода контейнеров (fine-grained locking)
Потокобезопасные контейнеры
3838
concurrent_hash_map
concurrent_vector
concurrent_queue
concurrent_vector
3939
void append(concurrent_vector<char> &vec, const char *str)
{size_t n = strlen(str) + 1;std::copy(str, str + n,
vec.begin() + vec.grow_by(n));}
Метод grow_by(n) безопасно добавляет n элементов
к вектору concurrent_vector
Взаимные исключения (Mutual exclusion)
4040
Взаимные исключения (mutual exclusion) позволяют
управлять количеством потоков, одновременно
выполняющих заданный участок кода
В Intel TBB взаимные исключения реализованы
средствами мьютексов (mutexes) и блокировок (locks)
Мьютекс (mutex) – это объект синхронизации,
который в любой момент времени может быть захвачен
только одним потоком, остальные потоки ожидают его
освобождения
Свойства мьютексов Intel TBB
4141
Scalable
Fair – справедливые мьютексы захватываются в порядке обращения к ним потоков (даже если следующий поток в очереди находится в состоянии сна; несправедливыемьютексы могут быть быстрее)
Recursive – рекурсивные мьютексы позволяют потоку захватившему мьютекс повторно его получить
Yield – при длительном ожидании мьютекса поток периодически проверяет его текущее состояние и снимает себя с процессора (засыпает, в GNU/Linux вызывается sched_yield(), а в Microsoft Windows – SwitchToThread())
Block – потока освобождает процессор до тех пор, пока не освободится мьютекс (такие мьютексы рекомендуется использовать при длительных ожиданиях)
Мьютексы Intel TBB
4242
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)
Мьютексы Intel TBB
4343
Mutex Scalable Fair RecursiveLongWait
Size
mutex OS dep. OS dep. No Blocks>= 3
words
recursive_mutex OS dep. OS dep. 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
spin_mutex
44
ListNode *FreeList;spin_mutex ListMutex;
ListNode *AllocateNode(){
ListNode *node;{
// Создать и захватить мьютекс (RAII)spin_mutex::scoped_lock lock(ListMutex);node = FreeList;if (node)
FreeList = node->next;} // Мьютекс автоматически освобождается if (!node)
node = new ListNode()return node;
}
44
spin_mutex
4545
void FreeNode(ListNode *node) {
spin_mutex::scoped_lock lock(ListMutex);node->next = FreeList;FreeList = node;
}
Конструктор scoped_lock ожидает освобождения мьютекса ListMutex
Структурный блок (операторные скобки {}) внутри AllocateNode
нужен для того, чтобы при выходе из него автоматически вызывался
деструктор класса scoped_lock, который освобождает мьютекс
Программная идиома RAII – Resource Acquisition Is Initialization
(получение ресурса есть инициализация)
spin_mutex
4646
ListNode *AllocateNode() {
ListNode *node;spin_mutex::scoped_lock lock;lock.acquire(ListMutex);node = FreeList;if (node)
FreeList = node->next;lock.release();if (!node)
node = new ListNode();return node;
}
Если защищенный блок (acquire-release) сгенерирует исключение, то release вызван не будет!
Используйте RAII если в пределах критической секции возможно возникновение исключительной ситуации
Атомарные операции (Atomic operations)
4747
Атомарная операция (Atomic operation) – это операций,
которая в любой момент времени выполняется только
одним потоком
Атомарные операции намного “легче” мьютексов –
не требуют блокирования потоков
TBB поддерживаем атомарные переменные
atomic<T> AtomicVariableName
Атомарные операции (Atomic operations)
4848
Операции над переменной 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
Атомарные операции (Atomic operations)
4949
atomic<int> counter;
unsigned int GetUniqueInteger() {
return counter.fetch_and_add(1);}
Атомарные операции (Atomic operations)
5050
atomic<int> Val;
int UpdateValue()
{
do {
v = Val;
newv = f(v);
} while(Val.compare_and_swap(newv, v) != v);
return v;
}
Аллокаторы памяти
5151
Intel TBB предоставляет два аллокатора
памяти (альтернативы STL std::allocator)
scalable_allocator<T> – обеспечивает параллельное
выделение памяти нескольким потокам
cache_aligned_allocator<T> – обеспечивает выделение
блоков памяти, выравненных на границу длины кеш-
линии (cacheline)
Это позволяет избежать ситуации когда потоки на разных
процессорах пытаются модифицировать разные слова
памяти, попадающие в одну строку кэша, и как следствие,
постоянно перезаписываемую из памяти в кеш
Аллокаторы памяти
5252
/* STL vector будет использовать аллокатор TBB */std::vector<int, cache_aligned_allocator<int> > v;
Ссылки
5353
James Reinders. Intel Threading Building Blocks.
– O'Reilly, 2007. – 336p.
Intel Threading Building Blocks Documentation //
http://software.intel.com/sites/products/documentation/docli
b/tbb_sa/help/index.htm