Русский
Русский
English
Статистика
Реклама

Boost::compute

Из песочницы Boost.Compute или параллельные вычисления на GPUCPU. Часть 1

15.08.2020 18:06:03 | Автор: admin
Привет, Хабр!

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

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

Всем удачи!
Подробнее..
Категории: C++ , C++17 , Boost::compute

Boost.Compute или параллельные вычисления на GPUCPU. Часть 2

16.08.2020 16:17:46 | Автор: admin

Вступление


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

Предыдущая часть понравилась многим, поэтому я снова перелопатил половину документации boost и нашёл о чем написать. Очень странно что вокруг boost.compute нету такого же ажиотажа как и вокруг boost.asio. Ведь достаточно, того эта библиотека кроссплатформенная, так ещё и предоставляет удобный (в рамках c++) интерфейс взаимодействия с параллельными вычислениями на GPU и CPU.

Все части




Содержание


  • Платформы
  • Асинхронные операции
  • Пользовательские функции
  • Сравнение скорости работы разных устройств в разных режимах
  • Заключение


Платформы


Не смотря на то что boost.compute кроссплатформенный инструмент, работает он не со всеми вычислительными устройствами. Возможно всё зависит от версии OpenCL, точно сказать не могу, но проверить у себя, с чем вы сможете работать, можно следующим кодом:
auto platforms = compute::system::platforms();for (size_t i = 0; i < platforms.size(); i++){cout << platforms[i].name() << endl;}


Я получил такой результат:


Экспериментальным путём я выяснил что собранные на моём ноутбуке программы будут запускаться лишь на устройствах таких же производителей.

Асинхронные операции


Казалось бы, куда ещё быстрее? один из способов ускорить работу с контейнерами пространства имён compute это использование асинхронных функций. Boost.compute предоставляет нам несколько инструментов. Из них класс compute::future для контроля использования функций и функции copy_async(), fill_async() для копирования или заполнения массива. Конечно, существуют ещё и инструменты для работы с событиями, но их рассматривать нет необходимости. Дальше будет пример использования всего выше перечисленного:
auto device = compute::system::default_device();auto context = compute::context::context(device);auto queue = compute::command_queue(context, device);std::vector<int> vec_std = {1, 2, 3};compute::vector<int> vec_compute(vec_std.size(), context);compute::vector<int> for_filling(10, context);int num_for_fill = 255;compute::future<void> filling = compute::fill_async(for_filling.begin(), for_filling.end(), num_for_fill, queue); // асинхронно заполняет заданный векторcompute::future<void> copying = compute::copy_async(vec_std.begin(), vec_std.end(), vec_compute.begin(), queue); // асинхронно копирует следующий векторfilling.wait();copying.wait();


Пояснять тут особо нечего. Первые три строчки стандартная инициализация необходимых классов, потом два векторы для копирования, вектор для заполнения, переменная которой будем заполнять предыдущий вектор и непосредственно функции для заполнения и копирования соответственно. Потом дожидаемся их выполнения.
Для тех, кто работал с std::future из STL, тут абсолютно всё тоже самое, только в другом пространстве имён и нет аналога std::async().

Пользовательские функции для вычислений



В предыдущей части я сказал, что поясню как использовать свои собственные методы для обработки массива данных. Я насчитал 3 способа как это можно сделать: использовать макрос, использовать make_function_from_source<>() и использовать специальный фреймворк для лямбда выражений.

Начну с самого первого варианта макроса. Сначала приложу пример кода а потом поясню как работает.
BOOST_COMPUTE_FUNCTION(float, add,(float x, float y),{ return x + y; });

Первым аргументом указываем тип возвращаемого значения, потом название функции, её аргументы и тело функции. Дальше под именем add, данную функцию можно использовать например в функции compute::transform(). Использование этого макроса очень похоже на обычное лямбда выражение, но я проверял, они работать не будут.

Второй и, наверное, самый сложный способ очень похож на первый. Я смотрел код предыдущего макроса и оказалось, что он использует именно второй способ.
compute::function<float(float)> add = compute::make_function_from_source<float(float)>("add", "float add(float x, float y) { return x + y; }");

Здесь всё очевидней чем может показаться на первый взгляд, функция make_function_from_source(), использует всего два аргумента, один из которых название функции, а второй её реализация. После объявления функции её можно использовать так же как и после реализации макросом.

Ну и последний вариант это фреймворк для лямбда выражений. Пример использования:
compute::transform(com_vec.begin(),      com_vec.end(),      com_vec.begin(),      compute::_1 * 2,      queue);

Четвёртым аргументом мы указываем что хотим умножить каждый элемент из первого вектора на 2, всё достаточно просто и делается на месте.

Этим же способом можно указывать логические выражения. Например в методе compute::count_if():
std::vector<int> source_std = { 1, 2, 3 };compute::vector<int> source_compute(source_std.begin() ,source_std.end(), queue);auto counter = compute::count_if(source_compute.begin(), source_compute.end(), compute::lambda::_1 % 2 == 0, queue);

Таким образом мы посчитали все чётные числа в массиве, counter будет равен единице.

Сравнение скорости работы разных устройств в разных режимах



Ну и последнее, про что я хотел бы написать в этой статье, это сравнение скорости обработки данных на разных устройствах и в разных режимах(только для CPU). это сравнение докажет, когда есть смысл использовать GPU для вычислений и параллельные вычисления в целом.

Тестировать я буду так: с помощью compute для всех устройств вызову функцию compute::sort() для того чтоб отсортировать массив из 100 млн. значений типа float. Для теста однопоточного режима вызову std::sort для массива такого же размера. Для каждого устройства засеку время в миллисекундах с помощью стандартной библиотеки chrono и выведу всё в консоль.

Получился такой результат:


Теперь сделаю всё тоже самое только для тысячи значений. На этот раз время будет в микросекундах.


На этот раз процессор в однопоточном режиме опередил всех. Из этого делаем вывод что такого рода операции стоит делать только когда речь идёт о действительно больших данных.

Заключение


Итак, прочитав эту статью, вы научились использовать асинхронные функции для копирования массивов и их заполнения. Узнали какими способами можно использовать свои собственные функции для проведения вычислений над данными. А также наглядно увидели когда стоит использовать GPU или CPU для параллельных вычислений, а когда можно обойтись одним потоком.

Буду рад позитивным отзывам, спасибо за уделённое время!

Всем удачи!
Подробнее..
Категории: C++ , Gpu вычисления , Boost::compute

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru