Boost.Compute или параллельные вычисления на GPU/CPU. Часть 1

от автора

Привет, Хабр!

По моим меркам я уже достаточно давно пишу код на C++, но до этого времени ещё не сталкивался с задачами, связанными с параллельными вычислениями. Я не увидел ни одной статьи о библиотеке Boost.Compute, поэтому эта статья будет именно о ней.

Содержание

  • Что такое boost.compute
  • Проблемы с подключением boost.compute к проекту
  • Введение в boost.compute
  • Основные классы compute
  • Приступаем к работе
  • Заключение

Что такое boost.compute

Данная c++ библиотека предоставляет простой высокоуровневый интерфейс для взаимодействия с многоядерными CPU и GPU вычислительными устройствами. Эта библиотека была впервые добавлена в boost в версии 1.61.0 и поддерживается до сих пор.

Проблемы с подключением boost.compute к проекту

И так, я столкнулся с некоторыми проблемами при использовании этой библиотеки. Одной из них было то, что без OpenCL библиотека попросту не работает. Компилятор выдаёт следующую ошибку:

image

После подключения всё должно скомпилироваться корректно.

На счёт библиотеки 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;  

Таким образом мы получили имя устройства, на котором будем производить вычисления. Результат (у вас может быть что-то другое):

image

Приступаем к работе

Рассмотрим функции 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; 

При запуске приведённого выше кода, вы должны увидеть такой результат:

image

Я остановился именно на этих двух методах потому, что они хорошо показывают примитивную работу с параллельными вычислениями без всего лишнего.

И так, функция 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, а также можете предотвратить некоторые ошибки при работе с этой библиотекой.

Буду рад позитивному фидбэку. Спасибо за уделённое время.

Всем удачи!

ссылка на оригинал статьи https://habr.com/ru/post/515262/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *