Россия |
Гиперобъекты Intel® CilkTM Plus
Презентацию к лекции Вы можете скачать здесь.
Область видимости переменных в многопоточных программах. Проблемы и решения
Область видимости – один из важнейших атрибутов переменной. Если область видимости ограничена, переменная называется локальной. Если область видимости переменной совпадает с программой, переменная называется глобальной.
В многопоточном программировании область видимости получает дополнительное измерение – видимость между потоками. Исполнение параллельной программы перестаёт быть локальным!
Pro
Использование глобальных переменных позволяет избежать «раздувания» списков параметров. Глобальными объявляются часто используемые параметры.
Contra
Побочные эффекты использования глобальных переменных могут препятствовать эффективной реализации параллелизма.
Пусть два оператора из разных потоков имеют доступ к одной переменной x. Тогда могут существовать 3 типа гонок за данными:
Оператор 1 | Оператор 2 | Тип «гонок за данными» |
---|---|---|
чтение | чтение | отсутствуют |
чтение | запись | по чтению |
запись | чтение | по чтению |
запись | запись | по записи |
Размер машинного слова может влиять на наличие гонок – при работе с упакованными структурами данных. Пример:
struct{ char a; char b; } x;
Наличие гонок может зависеть от уровня оптимизации.
Побочные эффекты
Гонки за данными – возникают при одновременном доступе из разных потоков к одной переменной. Отрицательный эффект – утрата детерминизма в поведении программы, утрата корректности. Это происходит, если
- хотя бы один поток производит запись в общую переменную;
- доступ к переменной происходит одновременно.
Как избежать гонок за данными
Синхронизация доступа к переменной.
Использование локальных относительно потоков переменных.
Гиперобъекты в Intel® Cilk™ Plus
Гиперобъекты (редукторы) в Intel® Cilk™ Plus – реализуют механизм разрешения гонок за данными.
Гиперобъект (редуктор) – в простейшем случае объект, с которым ассоциированы: значение, начальное значение, функция приведения.
Обращаться с редуктором надо как с объектом. Например, запрещено прямое копирование – результат такого копирования не определён.
При работе с редукторами не надо использовать блокировки => увеличивается производительность.
Редукторы сохраняют последовательную семантику: результат параллельной программы совпадает с результатом последовательной программы.
Редукторы можно использовать не только в циклах.
Переменная может быть описана как редуктор относительно ассоциативной операции (сложение, умножение, логическое И, объединение списков и другие). Требуется также использование соответствующего заголовочного файла. Пример:
#include <cilk/cilk.h> #include <cilk/reducer_min.h> template <typename T> size_t IndexOfMin(T array[], size_t n) { cilk::reducer_min_index<size_t, T> r; cilk_for (int i = 0; i < n; ++i) r.min_of(i, array[i]); return r.get_index(); }
Изображение переменной – это её экземпляр. Потоки могут работать с переменной как с обычной нелокальной переменной.
При создании потока он получает собственное изображение переменной. В многопоточном приложении для переменной создаётся набор изображений.
Система времени исполнения Cilk Plus координирует работу с изображениями переменной и объединяет их в точке объединения потоков (отсюда название - редукторы).
Когда остаётся единственное изображение, оно устойчиво и значение переменной может быть извлечено из этого изображения.
Редуктор суммирования:
Если в процессе выполнения Cilk-программы не происходит захвата работы, редуктор ведёт себя как обычная переменная.
Если происходит захват работы, потомок и продолжение получают собственные изображения.
Такую семантику иногда называют «ленивой».
#include <cilk/cilk.h> #include <cilk/reducer_opadd.h> class CilkForSum : public Sum { public: virtual double FindSum(SimpleArray &data) { cilk::reducer_opadd<double> result(0); cilk_for(int i=0; i<data.GetSize(); i++) result += operation(data[i]); return result.get_value(); } };
Предопределённые редукторы
Редуктор/заголовок | Инициализация | Описание |
---|---|---|
reducer_list_append <cilk/reducer_list.h> | Пустой список | Объединение списков добавлением в конец |
reducer_list_prepend <cilk/reducer_list.h> | Пустой список | Объединение списков добавлением в начало |
reducer_max <cilk/reducer_max.h> | Аргумент конструктора | Нахождение максимального значения |
reducer_max_index <cilk/reducer_max.h> | Аргумент конструктора | Нахождение максимального значения и его индекса в массиве |
reducer_min <cilk/reducer_min.h> | Аргумент конструктора | Нахождение минимального значения |
reducer_min_index <cilk/reducer_min.h> | Аргумент конструктора | Нахождение минимального значения и его индекса в массиве |
reducer_opadd <cilk/reducer_opadd.h> | 0 | Суммирование |
reducer_opand <cilk/reducer_opand.h> | 1/true | Логическое И |
reducer_opor <cilk/reducer_opor.h> | 0/false | Логическое ИЛИ |
reducer_opxor <cilk/reducer_opxor.h> | 0/false | Логическое исключающее ИЛИ |
reducer_ostream <cilk/reducer_ostream.h> | Аргумент конструктора | Параллельный поток вывода |
reducer_basic_string <cilk/reducer_string.h> | Пустая строка | Создание строки с помощью конкатенации |
reducer_string <cilk/reducer_string.h> | Пустая строка | Создание строки с помощью конкатенации |
reducer_wstring <cilk/reducer_string.h> | Пустая строка | Создание строки с помощью конкатенации |
Список заголовочных файлов
reducer.h reducer_list.h reducer_max.h reducer_min.h reducer_opadd.h reducer_opand.h reducer_opor.h reducer_opxor.h reducer_ostream.h reducer_string.h
cilk::reducer_opadd<float> sum; void f( int m ) { sum += m; } float g() { cilk_spawn f(1); f(2); cilk_sync; return sum.get_value(); }
Еще один пример:
cilk::reducer_opadd<float> sum = 0; ... cilk_for( size_t i=1; i<n; ++i ) sum += f(i); ... = sum.get_value();
Пример
#include <cilk/cilk.h> #include <cilk/reducer_opadd.h> #include <iostream> #include <math.h> … // CilkForSum class uses cilk_for keyword to compute the sum in parallel // cilk::reducer_opadd class is used for sync class CilkForSum : public Sum { public: virtual double FindSum(SimpleArray &data) { cilk::reducer_opadd<double> result(0); cilk_for(int i=0; i<data.GetSize(); i++) result += operation(data[i]); return result.get_value(); } };
Пользовательские гиперобъекты
Редуктор, определённый пользователем, состоит из 4-х логических частей:
- View – класс, приватные данные редуктора. Конструктор должен инициализировать изображение.
- Monoid – класс. Множество значений, ассоциативная операция на этом множестве и инициализирующее значение.
- Гиперобъект – изображение для каждого потока.
- Остальная часть редуктора – описывает, как выполняется доступ к данным и их модификация. get_value() возвращает значение.
Пример
#include <cilk/reducer.h> class point { // Здесь – определение класса point }; class point_holder { struct Monoid: cilk::monoid_base<point> { static void reduce (point *left, point *right) {} }; private: cilk::reducer<Monoid> imp_; public: point_holder() : imp_() {} void set(int x, int y) { point &p = imp_.view(); p.set(x, y); } bool is_valid() { return imp_.view().is_valid(); } int x() { return imp_.view().x(); } int y() { return imp_.view().y(); } };
Cilk Plus и Cilkscreen
Cilk Plus позволяет выявлять гонки за данными независимо от числа потоков, при умеренных накладных расходах (памяти и процессорного времени).
Cilkscreen – инструмент выявления гонок за данными.