Привет!
По моим меркам я уже достаточно давно пишу код на C++, но до этого времени ещё не сталкивался с задачами, связанными с параллельными вычислениями. Я не увидел ни одной статьи о библиотеке 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 <boost/compute.hpp>
using namespace boost;
Стоит подметить, что обычные контейнеры из stl не подойдут для использования в алгоритмах пространства имён compute. Вместо них существуют специально созданные контейнеры которые не конфликтуют с стандартными. Пример кода:
std::vector<float> std_vector(10);
compute::vector<float> 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<float> host_vec = {1, 4, 9};
compute::vector<float> com_vec(host_vec.begin(), host_vec.end(), queue);
// передавая в аргументы начальный и конечный указатель предыдущего вектора можно не
//использовать функцию copy()
compute::vector<float> buff_result(host_vec.size(), context);
transform(com_vec.begin(), com_vec.end(), buff_result.begin(), compute::sqrt<float>(), queue);
std::vector<float> 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<float>(),queue);
cout << "Reducing result: " << reduce_result << endl;
При запуске приведённого выше кода, вы должны увидеть такой результат:
Я остановился именно на этих двух методах потому, что они хорошо показывают примитивную работу с параллельными вычислениями без всего лишнего.
И так, функция transform() используется для того, чтобы изменить массив данных,(или два массива, если мы их передаём) применяя одну функцию ко всем значениям.
transform(com_vec.begin(),
com_vec.end(),
buff_result.begin(),
compute::sqrt<float>(),
queue);
Перейдём к разбору аргументов, первыми двумя аргументами мы передаём вектор входных данных, третьим аргументом передаём указатель на начало вектора, в который мы запишем результат, следующим аргументом мы указываем, что нам нужно сделать. В примере выше мы используем одну из стандартных функций обработки векторов, а именно извлекаем квадратный корень. Конечно, можно написать и кастомную функцию, boost предоставляет нам целых два способа, но это уже материал для следующей части(если такая вообще будет). Ну и последним аргументом мы передаём объект класса compute::command_queue, про который я рассказывал выше.
Следующая функция reduce(), тут все немного интереснее. Этот метод возвращает результат применения четвёртого аргумента ко всем элементам вектора.
compute::reduce(com_vec.begin(),
com_vec.end(),
&reduce_result,
compute::plus<float>(),
queue);
Сейчас поясню на примере, код выше можно сравнить с таким уравнением:
$inline$1 + 4 + 9$inline$
В нашем случае мы получаем суму всех элементов массива.
Заключение
Ну вот и всё, думаю этого хватит для того, чтоб проводить простые операции над большими данными. Теперь вы можете использовать примитивный функционал библиотеки boost.compute, а также можете предотвратить некоторые ошибки при работе с этой библиотекой.
Буду рад позитивному фидбэку. Спасибо за уделённое время.
Всем удачи!
Автор: Mikhail Didur