Date post: | 05-Dec-2014 |
Category: |
Documents |
Upload: | mikhail-kurnosov |
View: | 1,080 times |
Download: | 4 times |
Лекция 8:
Intel Threading Building Blocks
(Multithreading programming)
Курносов Михаил Георгиевич
к.т.н. доцент Кафедры вычислительных систем
Сибирский государственный университет
телекоммуникаций и информатики
http://www.mkurnosov.net
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 2013 – Intel TBB v.4.2
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
Таймеры, планировщик задач
Состав Intel TBB
55
Алгоритмы: 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
66
Intel TBB позволяет абстрагироваться от низкоуровневых
потоков и распараллеливать программу в терминах
параллельно выполняющихся задач (task parallelism)
Задачи TBB “легче” потоков операционной системы
Планировщик TBB использует механизм “work stealing”
для распределения задач по потокам
Все компоненты Intel TBB определены в пространстве
имен C++ (namespace) “tbb”
Компиляция программ с Intel TBB
77
$ 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!
88
Intel TBB: Hello World! (продолжение)
99
int main( ){
tbb::task_group tg;
tg.run(MyTask("1")); // Spawn tasktg.run(MyTask("2")); // Spawn tasktg.wait(); // Wait tasks
return 0;}
Компиляция и запуск tbb_hello
1010
$ 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
Инициализация библиотеки
1111
Любой поток использующий алгоритмы или планировщик TBB
должен иметь инициализированный объект
tbb::task_scheduler_init
TBB >= 2.2 автоматически инициализирует планировщик
Явная инициализация планировщика позволяет:
управлять когда создается и уничтожается планировщик
устанавливать количество используемых потоков
выполнения
устанавливать размер стека для потоков выполнения
Инициализация библиотеки
1212
#include <tbb/task_scheduler_init.h>
int main() {
tbb::task_scheduler_init init;
return 0;}
Явная инициализация планировщика
Инициализация библиотеки
1313
Конструктор класса 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)
Положительное целое – количество потоков
Инициализация библиотеки
1414
#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;
}
Распараллеливание циклов
1515
В TBB реализованы шаблоны параллельных алгоритмов
parallel_for
parallel_reduce
parallel_scan
parallel_do
parallel_for_each
parallel_pipeline
parallel_sort
parallel_invoke
parallel_for
1616
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
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
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
1919
Класс blocked_range(begin, end, grainsize) описывает одномерное
пространство итераций
В Intel TBB доступно описание многомерных пространств итераций
(blocked_range2d, ...)
affinity_partitioner
2020
Класс 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)
2121
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)
2222
Анонимная функция (лямбда-функция, С++11) [=] – захватить все автоматические переменные (const blocked_range …) – аргументы функции { ... } – код функции
parallel_reduce
2323
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
2424
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
2525
parallel_sort
2626
void parallel_sort(RandomAccessIterator begin,
RandomAccessIterator end,
const Compare& comp);
parallel_sort позволяет упорядочивать последовательности
элементов
Применяется детерминированный алгоритм
нестабильной сортировки с трудоемкостью O(nlogn) –
алгоритм не гарантирует сохранения порядка следования
элементов с одинаковыми ключами
parallel_sort
2727
#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)
2828
Intel TBB позволяет абстрагироваться от реальных
потоков операционной системы и разрабатывать
программу в терминах параллельных задач
(task-based parallel programming)
Запуск TBB-задачи примерно в 18 раз быстрее запуска
потока POSIX в GNU/Linux (в Microsoft Windows
примерно в 100 раз быстрее)
В отличии от планировщика POSIX-потоков в GNU/Linux
планировщик TBB реализует “не справедливую” (unfair)
политику распределения задач по потокам
Числа Фибоначчи: sequential version
2929
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
3030
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
3131
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
3232
Граф задачи (Task graph)
3333
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)
3434
Каждый поток поддерживает дек готовых к выполнению
задач (deque, двусторонняя очередь)
Планировщик использует комбинированный алгоритма
на основе обход графа задач в ширину и глубину
Task E
Task D
Top:
Oldest task
Bottom:
Youngest
Task
Планирование задач (Task scheduling)
3535
Листовые узлы в графе задач – это задачи готовые
к выполнению (ready task, они не ожидают других)
Потоки могу захватывать (steal) задачи из чужих деков
(с их верхнего конца)
Task E
Task D
Top:
Oldest task
Bottom:
Youngest
Task
Top:
Oldest task
Bottom:
Youngest
Task
Выбор задачи из дека
3636
Задача для выполнения выбирается одним из следующих
способов (в порядке уменьшения приоритета):
1. Выбирается задача, на которую возвращен указатель
методом execute предыдущей задачи
2. Выбирается задача с нижнего конца (bottom) дека потока
3. Выбирается первая задача из дека (с его верхнего конца)
случайно выбранного потока – work stealing
Помещение задачи в дек потока
3737
Задачи помещаются в дек с его нижнего конца
В дек помещается задача порожденная методом spawn
Задача может быть направлена на повторное выполнение
методом task::recycle_to_reexecute
Задача имеет счетчик ссылок (reference count)
равный нулю – все дочерние задачи завершены
Потокобезопасные контейнеры
3838
Intel TBB предоставляет классы контейнеров
(concurrent containers), которые корректно могут
обновляться из нескольких потоков
Для работы в многопоточной программе со стандартными
контейнерами STL доступ к ним необходимо защищать
блокировками (мьютексами)
Особенности Intel TBB:
o при работе с контейнерами применяет алгоритмы
не требующие блокировок (lock-free algorithms)
o при необходимости блокируются лишь небольшие
участки кода контейнеров (fine-grained locking)
Потокобезопасные контейнеры
3939
concurrent_hash_map
concurrent_vector
concurrent_queue
concurrent_vector
4040
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)
4141
Взаимные исключения (mutual exclusion) позволяют
управлять количеством потоков, одновременно
выполняющих заданный участок кода
В Intel TBB взаимные исключения реализованы
средствами мьютексов (mutexes) и блокировок (locks)
Мьютекс (mutex) – это объект синхронизации,
который в любой момент времени может быть захвачен
только одним потоком, остальные потоки ожидают его
освобождения
Свойства мьютексов Intel TBB
4242
Scalable
Fair – справедливые мьютексы захватываются в порядке
обращения к ним потоков (даже если следующий поток
в очереди находится в состоянии сна; несправедливые
мьютексы могут быть быстрее)
Recursive – рекурсивные мьютексы позволяют
потоку захватившему мьютекс повторно его получить
Yield – при длительном ожидании мьютекса поток
периодически проверяет его текущее состояние и снимает
себя с процессора (засыпает, в GNU/Linux вызывается
sched_yield(), а в Microsoft Windows – SwitchToThread())
Block – потока освобождает процессор до тех пор, пока
не освободится мьютекс (такие мьютексы рекомендуется
использовать при длительных ожиданиях)
Мьютексы Intel TBB
4343
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
4444
Mutex Scalable Fair RecursiveLong
WaitSize
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
45
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;
}
45
spin_mutex
4646
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
4747
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)
4848
Атомарная операция (Atomic operation) – это операций,
которая в любой момент времени выполняется только
одним потоком
Атомарные операции намного “легче” мьютексов –
не требуют блокирования потоков
TBB поддерживаем атомарные переменные
atomic<T> AtomicVariableName
Атомарные операции (Atomic operations)
4949
Операции над переменной 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)
5050
atomic<int> counter;
unsigned int GetUniqueInteger() {
return counter.fetch_and_add(1);}
Атомарные операции (Atomic operations)
5151
atomic<int> Val;
int UpdateValue()
{
do {
v = Val;
newv = f(v);
} while(Val.compare_and_swap(newv, v) != v);
return v;
}
Аллокаторы памяти
5252
Intel TBB предоставляет два аллокатора
памяти (альтернативы STL std::allocator)
scalable_allocator<T> – обеспечивает параллельное
выделение памяти нескольким потокам
cache_aligned_allocator<T> – обеспечивает выделение
блоков памяти, выравненных на границу длины кеш-
линии (cacheline)
Это позволяет избежать ситуации когда потоки на разных
процессорах пытаются модифицировать разные слова
памяти, попадающие в одну строку кэша, и как следствие,
постоянно перезаписываемую из памяти в кеш
Аллокаторы памяти
5353
/* STL vector будет использовать аллокатор TBB */std::vector<int, cache_aligned_allocator<int> > v;
Ссылки
5454
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