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

в 13:26, , рубрики: boost::compute, c++, c++17

Привет!

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

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

Всем удачи!

Автор: Mikhail Didur

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js