[Из песочницы] Boost.Compute или параллельные вычисления на GPU/CPU
Привет, Хабр!
По моим меркам я уже достаточно давно пишу код на C++ (почти 3 года), но до этого времени ещё не сталкивался с задачами, связанными с параллельными вычислениями. Я не увидел ни одной статьи о библиотеке Boost.Compute, поэтому эта статья будет именно о ней.
Содержание
- Что такое boost.compute
- Проблемы с подключением boost.compute к проекту
- Введение в boost.compute
- Основные классы compute
- Приступаем к работе
- Заключение
Что такое boost.compute
Данная c++ библиотека предоставляет простой высокоуровневый интерфейс для взаимодействия с многоядерными CPU и GPU вычислительными устройствами. Эта библиотека была впервые добавлена в boost в версии 1.61.0 и поддерживается до сих пор.
Проблемы с подключением boost.compute к проекту
И так, я столкнулся с некоторыми проблемами при использовании этой библиотеки. Одной из них было то, что без OpenCL библиотека попросту не работает. Компилятор выдаёт следующую ошибку:
После подключения всё должно скомпилироваться корректно.
На счёт библиотеки boost, её можно скачать и подключить к проекту Visual Studio с помощью менеджера пакетов NuGet.
Введение в boost.compute
После установки всех необходимых компонентов можно рассмотреть простые куски кода. Для корректной работы достаточно включить модуль compute таким образом:
#include
using namespace boost;
Стоит подметить, что обычные контейнеры из stl не подойдут для использования в алгоритмах пространства имён compute. Вместо них существуют специально созданные контейнеры которые не конфликтуют с стандартными. Пример кода:
std::vector std_vector(10);
compute::vector compute_vector(std_vector.begin(), std_vector.end(), queue);
// пока не обращайте внимания на третий аргумент, к нему мы вернёмся позже.
Для конвертации обратно в std: vector можно использовать функцию copy ():
compute::copy(compute_vector.begin(), compute_vector.end(), std_vector.begin(), queue);
Основные классы compute
Библиотека насчитывает в себе три вспомогательных класса, которых для начала хватит для вычислений на видеокарте и/или процессоре:
- compute: device (будет определять с каким именно устройством мы будем работать)
- compute: context (объект данного класса хранит в себе ресурсы OpenCL, включая буферы памяти и другие объекты)
- compute: command_queue (предоставляет интерфейс для взаимодействия с вычислительным устройством)
Объявить это всё дело можно таким образом:
auto device = compute::system::default_device(); // устройство по умолчанию это видеокарта
auto context = compute::context::context(device); // обычное объявление переменной
auto queue = compute::command_queue(context, device); // аналогично к предыдущему
Даже только с помощью первой строчки кода выше можно убедится что всё работает как нужно, запустив следующий код:
std::cout << device.name() << std::endl;
Таким образом мы получили имя устройства, на котором будем производить вычисления. Результат (у вас может быть что-то другое):
Приступаем к работе
Рассмотрим функции trasform () и reduce () на примере:
std::vector host_vec = {1, 4, 9};
compute::vector com_vec(host_vec.begin(), host_vec.end(), queue);
// передавая в аргументы начальный и конечный указатель предыдущего вектора можно не
//использовать функцию copy()
compute::vector buff_result(host_vec.size(), context);
transform(com_vec.begin(), com_vec.end(), buff_result.begin(), compute::sqrt(), queue);
std::vector transform_result(host_vec.size());
compute::copy(buff_result.begin(), buff_result.end(), transform_result.begin(), queue);
cout << "Transforming result: ";
for (size_t i = 0; i < transform_result.size(); i++)
{
cout << transform_result[i] << " ";
}
cout << endl;
float reduce_result;
compute::reduce(com_vec.begin(), com_vec.end(), &reduce_result, compute::plus(),queue);
cout << "Reducing result: " << reduce_result << endl;
При запуске приведённого выше кода, вы должны увидеть такой результат:
Я остановился именно на этих двух методах потому, что они хорошо показывают примитивную работу с параллельными вычислениями без всего лишнего.
И так, функция transform () используется для того, чтобы изменить массив данных,(или два массива, если мы их передаём) применяя одну функцию ко всем значениям.
transform(com_vec.begin(),
com_vec.end(),
buff_result.begin(),
compute::sqrt(),
queue);
Перейдём к разбору аргументов, первыми двумя аргументами мы передаём вектор входных данных, третьим аргументом передаём указатель на начало вектора, в который мы запишем результат, следующим аргументом мы указываем, что нам нужно сделать. В примере выше мы используем одну из стандартных функций обработки векторов, а именно извлекаем квадратный корень. Конечно, можно написать и кастомную функцию, boost предоставляет нам целых два способа, но это уже материал для следующей части (если такая вообще будет). Ну и последним аргументом мы передаём объект класса compute: command_queue, про который я рассказывал выше.
Следующая функция reduce (), тут все немного интереснее. Этот метод возвращает результат применения четвёртого аргумента ко всем элементам вектора.
compute::reduce(com_vec.begin(),
com_vec.end(),
&reduce_result,
compute::plus(),
queue);
Сейчас поясню на примере, код выше можно сравнить с таким уравнением:
$inline$1 + 4 + 9$inline$
В нашем случае мы получаем суму всех элементов массива.
Заключение
Ну вот и всё, думаю этого хватит для того, чтоб проводить простые операции над большими данными. Теперь вы можете использовать примитивный функционал библиотеки boost.compute, а также можете предотвратить некоторые ошибки при работе с этой библиотекой.
Буду рад позитивному фидбэку. Спасибо за уделённое время.
Всем удачи!