С появлением новых Стандартов C++ становится заметным оживление интереса разработчиков к языку. Интерес также обеспечивается возможностями, которые несут нам новые архитектуры и технологии. Нужда в эффективных параллельных алгоритмах и приложениях влечет за собой нужду в библиотеках и фреймворках, способных правильно адаптировать и масштабировать нагрузку по всей вычислительной системе. В докладе Антон расскажет о решающей данные проблемы библиотеке HPX, основанной на новой модели исполнения ParalleX, имеющей совместимый интерфейс с C++11/14/17 thread библиотекой, и сильно расширяющей ее по различным векторам. Незатронутыми не останутся вкусности параллельного мира из C++17 - технические спецификации Concurrency и Parallelism!
2. HPX
2HPX — Runtime System for Parallel and Distributed Computing
• Theoretical foundation — ParalleX
• C++ conformant API
• asynchronous
• unified syntax for remote and local operations
• https://github.com/stellar-group/hpx
4. What is a future?
4HPX — Runtime System for Parallel and Distributed Computing
• Enables transparent synchronization with producer
• Hides thread notion
• Makes asynchrony manageable
• Allows composition of several asynchronous operations
(C++17)
• Turns concurrency into parallelism
future<T>
empty value exception
5. What is a future?
5HPX — Runtime System for Parallel and Distributed Computing
fffjj
jоо
оjj
= async(…);
executing another thread fut.get();
suspending consumer
resuming consumer
returning result
producing result
Consumer Producer
fut
6. hpx::future & hpx::async
6HPX — Runtime System for Parallel and Distributed Computing
• lightweight tasks
- user level context switching
- each task has its own stack
• task scheduling
- work stealing between cores
- user-defined task queue (fifo, lifo, etc.)
- enabling use of executors
7. Extending the future (N4538)
7HPX — Runtime System for Parallel and Distributed Computing
• future initialization
template <class T>
future<T> make_ready_future(T&& value);
• result availability
bool future<T>::is_ready() const;
8. Extending the future (N4538)
8HPX — Runtime System for Parallel and Distributed Computing
• sequential composition
template <class Cont>
future<result_of_t<Cont(T)>>
future<T>::then(Cont&&);
“Effects:
— The function creates a shared state that is associated with the returned
future object. Additionally, when the object's shared state is ready, the
continuation is called on an unspecified thread of execution…
— Any value returned from the continuation is stored as the result in the
shared state of the resulting future.”
9. Extending the future (N4538)
9HPX — Runtime System for Parallel and Distributed Computing
• sequential composition: HPX extension
template <class Cont>
future<result_of_t<Cont(T)>>
future<T>::then(hpx::launch::policy, Cont&&);
template <class Exec, class Cont>
future<result_of_t<Cont(T)>>
future<T>::then(Exec&, Cont&&);
10. Extending the future (N4538)
10HPX — Runtime System for Parallel and Distributed Computing
• parallel composition
template <class InputIt>
future<vector<future<T>>>
when_all(InputIt first, InputIt last);
template <class... Futures>
future<tuple<Futures...>>
when_all(Futures&&… futures);
11. Extending the future (N4538)
11HPX — Runtime System for Parallel and Distributed Computing
• parallel composition
template <class InputIt>
future<when_any_result<vector<future<T>>>>
when_any(InputIt first, InputIt last);
template <class... Futures>
future<when_any_result<tuple<Futures...>>>
when_any(Futures&&... futures);
12. Extending the future (N4538)
12HPX — Runtime System for Parallel and Distributed Computing
• parallel composition: HPX extension
template <class InputIt>
future<when_some_result<vector<future<T>>>>
when_some(size_t n, InputIt f, InputIt l);
template <class... Futures>
future<when_some_result<tuple<Futures...>>>
when_some(size_t n, Futures&&... futures);
13. Futurization?
13HPX — Runtime System for Parallel and Distributed Computing
• delay direct execution in order to avoid
synchronization
• code no longer executes result but generates an
execution tree representing the original algorithm
T foo(…){}
rvalue
T res = foo(…)
future<T> foo(…){}
make_ready_future(rvalue)
future<T> res = async(foo, …)
14. Example: recursive digital filter
14HPX — Runtime System for Parallel and Distributed Computing
• generic recursive filter
15. Example: recursive digital filter
15HPX — Runtime System for Parallel and Distributed Computing
• generic recursive filter
• single-pole high-pass filter
29. Futures on distributed systems
29HPX — Runtime System for Parallel and Distributed Computing
int calculate();
void foo()
{
std::future<int> result =
std::async(calculate);
...
std::cout << result.get() << std::endl;
...
}
30. Futures on distributed systems
30HPX — Runtime System for Parallel and Distributed Computing
int calculate();
void foo()
{
hpx::future<int> result =
hpx::async(calculate);
...
std::cout << result.get() << std::endl;
...
}
31. Futures on distributed systems
31HPX — Runtime System for Parallel and Distributed Computing
int calculate();
HPX_PLAIN_ACTION(calculate, calculate_action);
void foo()
{
hpx::future<int> result =
hpx::async(calculate);
...
std::cout << result.get() << std::endl;
...
}
32. Futures on distributed systems
32HPX — Runtime System for Parallel and Distributed Computing
int calculate();
HPX_PLAIN_ACTION(calculate, calculate_action);
void foo()
{
hpx::id_type where =
hpx::find_remote_localities()[0];
hpx::future<int> result =
hpx::async(calculate);
...
std::cout << result.get() << std::endl;
...
}
33. Futures on distributed systems
33HPX — Runtime System for Parallel and Distributed Computing
int calculate();
HPX_PLAIN_ACTION(calculate, calculate_action);
void foo()
{
hpx::id_type where =
hpx::find_remote_localities()[0];
hpx::future<int> result =
hpx::async(calculate_action{}, where);
...
std::cout << result.get() << std::endl;
...
}
34. Futures on distributed systems
34HPX — Runtime System for Parallel and Distributed Computing
Locality 1 Locality 2
future.get();
future
call to
hpx::async(…);
35. Futures on distributed systems
35HPX — Runtime System for Parallel and Distributed Computing
namespace boost { namespace math {
template <class T1, class T2>
some_result_type cyl_bessel_j(T1 v, T2 x);
}}
36. Futures on distributed systems
36HPX — Runtime System for Parallel and Distributed Computing
namespace boost { namespace math {
template <class T1, class T2>
some_result_type cyl_bessel_j(T1 v, T2 x);
}}
namespace boost { namespace math {
template <class T1, class T2>
struct cyl_bessel_j_action:
hpx::actions::make_action<
some_result_type (*)(T1, T2),
&cyl_bessel_j<T1, T2>,
cyl_bessel_j_action<T1, T2>
> {};
}}
37. Futures on distributed systems
37HPX — Runtime System for Parallel and Distributed Computing
int main()
{
boost::math::cyl_bessel_j_action<double, double>
bessel_action;
std::vector<hpx::future<double>> res;
for (const auto& loc : hpx::find_all_localities())
res.push_back(
hpx::async(bessel_action, loc, 2., 3.);
}
38. HPX task invocation overview
38HPX — Runtime System for Parallel and Distributed Computing
R f(p…)
Synchronous
(returns R)
Asynchronous
(returns future<R>)
Fire & forget
(return void)
Functions f(p…); async(f, p…); apply(f, p…);
Actions
HPX_ACTION(f, a);
a{}(id, p…);
HPX_ACTION(f, a);
async(a{}, id, p…);
HPX_ACTION(f, a);
apply(a{}, id, p…);
C++
C++ stdlib
HPX
39. Writing an HPX component
39HPX — Runtime System for Parallel and Distributed Computing
struct remote_object
{
void apply_call();
};
int main()
{
remote_object obj{some_locality};
obj.apply_call();
}
40. Writing an HPX component
40HPX — Runtime System for Parallel and Distributed Computing
struct remote_object_component:
hpx::components::simple_component_base<
remote_object_component>
{
void call() const
{
std::cout << "hey" << std::endl;
}
HPX_DEFINE_COMPONENT_ACTION(
remote_object_component, call, call_action);
};
41. Writing an HPX component
41HPX — Runtime System for Parallel and Distributed Computing
struct remote_object_component:
hpx::components::simple_component_base<
remote_object_component>
{
void call() const
{
std::cout << "hey" << std::endl;
}
HPX_DEFINE_COMPONENT_ACTION(
remote_object_component, call, call_action);
};
HPX_REGISTER_COMPONENT(remote_object_component);
HPX_REGISTER_ACTION(remote_object_component::call_action);
42. Writing an HPX component
42HPX — Runtime System for Parallel and Distributed Computing
struct remote_object_component;
int main()
{
hpx::id_type where =
hpx::find_remote_localities()[0];
hpx::future<hpx::id_type> remote =
hpx::new_<remote_object_component>(where);
//prints hey on second locality
hpx::apply(call_action{}, remote.get());
}
43. Writing an HPX client for component
43HPX — Runtime System for Parallel and Distributed Computing
struct remote_object:
hpx::components::client_base<
remote_object, remote_object_component>
{
using base_type = ...;
remote_object(hpx::id_type where): base_type{
hpx::new_<remote_object_component>(where)}
{}
void apply_call() const
{
hpx::apply(call_action{}, get_id());
}
};
44. Writing an HPX client for component
44HPX — Runtime System for Parallel and Distributed Computing
int main()
{
hpx::id_type where =
hpx::find_remote_localities()[0];
remote_object obj{where};
obj.apply_call();
return 0;
}
45. Writing an HPX client for component
45HPX — Runtime System for Parallel and Distributed Computing
Locality 1 Locality 2
Global Address Space
struct remote_object_component:
simple_component_base<…>
struct remote_object:
client_base<…>
46. Writing multiple HPX clients
46HPX — Runtime System for Parallel and Distributed Computing
int main()
{
std::vector<hpx::id_type> locs =
hpx::find_all_localities();
std::vector<remote_object> objs {
locs.cbegin(), locs.cend()};
for (const auto& obj : objs)
obj.apply_call();
}
47. Writing multiple HPX clients
47HPX — Runtime System for Parallel and Distributed Computing
Locality 1 Locality 2 Locality N
Global Address Space
48. HPX: distributed point of view
48HPX — Runtime System for Parallel and Distributed Computing
50. HPX parallel algorithms
50HPX — Runtime System for Parallel and Distributed Computing
template<class ExecutionPolicy,
class InputIterator, class Function>
void for_each(ExecutionPolicy&& exec,
InputIterator first, InputIterator last,
Function f);
• Execution policy
sequential_execution_policy
parallel_execution_policy
parallel_vector_execution_policy
hpx(std)::parallel::seq
hpx(std)::parallel::par
hpx(std)::parallel::par_vec
51. HPX parallel algorithms
51HPX — Runtime System for Parallel and Distributed Computing
template<class ExecutionPolicy,
class InputIterator, class Function>
void for_each(ExecutionPolicy&& exec,
InputIterator first, InputIterator last,
Function f);
• Execution policy
sequential_execution_policy
parallel_execution_policy
parallel_vector_execution_policy
sequential_task_execution_policy
parallel_task_execution_policy
hpx::parallel::seq(task)
hpx::parallel::par(task)
HPX
hpx(std)::parallel::seq
hpx(std)::parallel::par
hpx(std)::parallel::par_vec
52. HPX map reduce algorithm example
52HPX — Runtime System for Parallel and Distributed Computing
template <class T, class Mapper, class Reducer>
T map_reduce(const std::vector<T>& input,
Mapper mapper, Reducer reducer)
{
// ???
}
53. HPX map reduce algorithm example
53HPX — Runtime System for Parallel and Distributed Computing
template <class T, class Mapper, class Reducer>
T map_reduce(const std::vector<T>& input,
Mapper mapper, Reducer reducer)
{
std::vector<T> temp(input.size());
std::transform(std::begin(input), std::end(input),
std::begin(temp), mapper);
return std::accumulate(std::begin(temp),
std::end(temp), T{}, reducer);
}
54. HPX map reduce algorithm example
54HPX — Runtime System for Parallel and Distributed Computing
template <class T, class Mapper, class Reducer>
future<T> map_reduce(const std::vector<T>& input,
Mapper mapper, Reducer reducer)
{
using namespace hpx::parallel;
auto temp = std::make_shared<std::vector>(
input.size());
auto mapped = transform(par(task), std::begin(input),
std::end(input), std::begin(*temp), mapper);
return mapped.then([temp, reducer](auto)
{
return reduce(par(task), std::begin(*temp),
std::end(*temp), T{}, reducer);
});
}
55. HPX map reduce algorithm example
55HPX — Runtime System for Parallel and Distributed Computing
template <class T, class Mapper, class Reducer>
future<T> map_reduce(const std::vector<T>& input,
Mapper mapper, Reducer reducer)
{
using namespace hpx::parallel;
return transform_reduce(par(task), std::begin(input),
std::end(input), mapper, T{}, reducer);
}
56. Thank you for your attention!
HPX — Runtime System for Parallel and Distributed Computing
• https://github.com/stellar-group/hpx
Editor's Notes
Всем привет! Меня зовут Антон Бикинеев, и я рад видеть столько умных лиц в аудитории, а также хочу поблагодарить Сергея за организацию мероприятия и за то, что дал познать красоту Новосибирска. Окей. Сегодня я собираюсь поговорить об HPX - библиотеке для параллельного и распределенного программирования, которую мы разрабатываем в Ste||ar-Group, которая расширяет новые стандарты языка по асинхронному функционалу и позволяет разрабатывать параллельные приложения любого масштаба, для смартфонов и для кластеров. Звучит захватывающе, правда?
Что же за зверь это, HPX?
Первое, что стоит сказать, система имеет твердый теоретический фундамент, а именно модель исполнения Parallex. В нашей группе, Stellar-Group, мы верим, что текущие стандарты параллельного программирования, такие как openmp или mpi, основаны на довольно старых моделях исполнения, не подходящих для сегодняшних петафлоп машин. Часто мы видим распределенные приложения, плохо скейлющиеся на таких мощных машинах. Тем самым немалая часть ресурсов и энергии расходуется впустую.
Другая важная особенность — совместимость с API из C++ стандартной библиотеки. API из стандартной библиотеки отлично подходит для задач ParalleX модели. Мы имеем futures, async, promises, мьютексы, кондишн вэриэбл и другие объекты синхронизации, а также реализации binderов, function, tuple и др. фич, которые могут сериализоваться. Что здорово, на мой взгляд, HPX обеспечивает единый синтаксис для локальных операций внутри одной машины а также для удаленных операций на других. Кому стало интересно го эхед и клонируйте библиотеку.
Что же за зверь это, HPX?
Первое, что стоит сказать, система имеет твердый теоретический фундамент, а именно модель исполнения Parallex. В нашей группе, Stellar-Group, мы верим, что текущие стандарты параллельного программирования, такие как openmp или mpi, основаны на довольно старых моделях исполнения, не подходящих для сегодняшних петафлоп машин. Что мы видим — это распределенные приложения, не расширяющиеся на такие мощных машинах. Тем самым большая часть энергии расходуется впустую.
Другая важная особенность — совместимость с API из C++ стандартной библиотеки. API из стандартной библиотеки отлично подходит для задач ParalleX модели. Мы имеем futures, async, promises, мьютексы, кондишн вэриэбл и другие объекты синхронизации, а также реализации binderов, function, tuple и др. фич, которые могут сериализоваться. Что здорово, на мой взгляд, HPX обеспечивает единый синтаксис для локальных операций внутри одной машины а также для удаленных операций на других. Кому стало интересно го эхед и складируйте библиотеку.
Первое, о чем я хочу поговорить — это future. Future, пожалуй самый главный объект синхронизации в HPX. Future представляет объект, который в момент обращения к нему может быть еще не вычислен. Прокси к объекту. Он может быть пустым, иметь значение, вычисленное или нет, а также хранить exception внутри себя, в случае если операция выполняющая вычисление значения выкинула исключение.
Тем самым мы находим future прекрасным механизмом для синхронизации потока продюсера и консюмера, позволяющим скрыть написание plain тредов, тем самым делая асинхронность управляемой. Новый TS для параллелизма определяет возможности для композиции асинхронных операций, о чем я поговорю попозже. Также хочу сказать, что future сводит идею concurrency к параллелизму. Конечно мы все равно можем бороться за объект внутри двух асинхронных операций и получить гонку и UB, но хочу заверить, что с каждым разом разрабатывая асинхронный task-based код начинаешь думать не о том как обеспечить эксклюзивный доступ к каким-нибудь данным, а как вернуть результат из асинхронной операции, при этом принося поменьше сайд эффектов. Окей, это наша точка зрения.
Это небольшая картинка показывает, как работает future и спавнится асинхронная задача с помощью async. Давайте представим, что эти кривые — это кернел треды, или ос-треды или посикс-треды, а используемые async и futures из стандартной библиотеки. Сначала консьюмер-тред вызывает async, который запускает асинхронную задачу в продюсер-треде. Потом на некоем этапе консьюмер-тред решает получить результат, но результат еще не готов и данный поток блочится, а систем скедэлер идет и запускает другой поток на этом, скажем, ядре. В некоторый момент продюсер-тред решает вернуть результат и возвращает его, пишет в шаред стэйт. Консьюмер-поток просыпается, получает результат из шаред стэйта и продолжает работу.
В чем проблема кернел тредов? Они дают большой оверхэд на создание и скедэлинг, так как само ядро должно управлять ими и скедэлить их, иметь тред контрол блок для каждого треда, чтобы поддерживать информацию о них. Тем самым кернел треды не подходят для создания асинхронных задач, где мы расспавниваем, скажем 1000000 тредов. на это уйдет ощутимое время, и, вдруг может что-то отвалиться. HPX предоставляет свои треды и свои совместимые по API futures и async, дающие так называемый fine-grain parallelism. HPX использует O(MxN) модель трединга, или гибридную модель трединга, в сушности которой M пользовательских тредов мапятся на N кернел тредов, где N обычно число единиц вычисления, число ядер процессора. С HPX мы получаем очень легковесные задачи, можем задать свой скедэлер, его политику first или last in first out.
Теперь я хочу рассказать о текущих пропозалах в 17й стандарт, о техникал спецификэйшне для concurrency и как он уже используются в HPX. TS определяет новые функции и одна из них, make_ready_future, предоставляет инициализацию уже готового future. Если вы помните, раньше, чтобы инициализировать каким-либо значением future, нам нужно было создать promise, вызвать у него set_value, затем получить future с тем же dhared state с помощью get_future. Теперь это может быть возможно с помощью одной операции.
Следующая функция is_ready дает возможность протестить результат на готовность, но я не сталкивался с ее явным использованием.
Теперь о гораздо более важной функции .then.
С момента стандартизации futures и async, т.е. с 11 года люди быстро ощутили нехватку возможности композицировать асинхронные операции, т.е. аттачить континуэйшны к futures. Континуэйшн предоставляет из себя функцию или callable object, который будет вызвана только тогда, когда исходное future станет готовым. Тем самым мы можем соединять несколько асинхронных операций с помощью .then(…).then(..).then(..) Такие подходы вы могли встречать во всяких асинхронных фреймворках вроде касабланки, майкрософт ppl и других.
Как я сказал, .then принимает коллэбл обджект, параметр у которого future на значение, которое холдит исходное future, т.е. если у нас есть future от int, в .then будет передан континуэйшн, у которого параметр future<int> или rvalue reference на future на int. В свою очередь функция возвращает другой future на тип, который возвращает континуэйшн. На самом деле есть еще правило implicit unwrapping, если континуэйшн возвращает future на int, то .then возвратит не future на future на int, а за анвраппит внутренний future и возвратит future на int. Удобно?
HPX, как всегда, расширяет функционал с помощью пары перегрузок для .then. Первая принимает launch::policy, точно так же, как делает это async, а вторая принимает экзэкьютор. Экзекьютор наверно можно назвать неким обобщением тред пула. Для них существует еще несколько пропозалов в стандарт, но, насколько знаю, они не заэксептены. Про них я только упомяну, говорить не буду.
Текующие стандарты также не дают возможность композицировать несколько futures. Это тоже распространенная операция во всяких асинхронных языках или фрэймворках. Отсутствие данных функций для асинхронного программирования сродни отсутствию операций И и ИЛИ в булевой алгебре.
Функция when_all асинхронно ждет завершения всех переданных ей futures и shared_futures. Первая ее перегрузка берет пару итераторов на начало и конец рейнджа и возвращает future на вектор из futures на value_type этого итератора. Этот возвращенный future станет готовым только тогда, когда все переданные future станут готовыми. Вторая перегрузка принимает вариадик тэмплэт из futures и тем самым позволяет передавать future на различные типы. Полиморфная перегрузка называется, если я не ошибаюсь. В свою очередь она возвращает future на tuple из переданных futures. Все понятно?
Существует еще when_any функция для параллельной композиции futures, которая принимает те же аргументы, но возвращает результат только тогда, когда одно из futures станет валидным. Возвратным значением является future на when_any_result где when_any_result — просто структура, холдящая вектор или тапл из исходных futures и size_t индекс future, который первым завершил выполнение.
HPX снова расширяет стандарт, обеспечивая более обобщенную функцию when_some, которая, как вы могли догадаться, возвращает результат такой же, как и when_any, но с when_some_result, где when_some_result снова холдит переданный контейнер либо вектор либо тапл и вектор из индексов futures, которые первые завершились.
В Stellar-Group была создана техника, которую я по-русски буду называть футуризацией. С помощью футуризации ваш код или функция теперь асинхронно откладывает исполнение и вместо непосредственного исполнения и вычисления генерирует представляющее этот алгоритм дерево (т.н. execution tree), каждая вершина которого представляет асинхронную операцию.
Для того чтобы футуризовать Вашу функцию, нужно изменить ее возвращаемое значение на future от предыдущего возвращаемого значения, все rvalue внутри функции заменить на make_ready_future, о которой говорили раньше, а вызовы внутренних функций заменить на асинхронные вызовы. Но не только. Давайте рассмотрим пример.
Надеюсь, Вы помните обобщенную формулу цифрового фильтра, но я очень смутно помню, хотя учил теорию цифровой обработки сигналов два года назад. Эта формула представляет из себя обобщенный рекурсивный цифровой фильтр, систему, зависящую от параметров — входного дискретного сигнала x, а также коэффициентов a0,a1 и т.д. и b1 и т.д. Из формулы видно, что она представляет из себя рекуррентное соотношение, поскольку результат выходного сигнала в момент времени n зависит от результатов в предыдущие моменты времени.
И давайте сначала попробуем посчитать очень простой фильтр — фильтр высоких частот, который имеет следующие параметры отличные от нуля и может быть представлен следующей схемой. Как видно, результат работы фильтра в момент времени n зависит только от результата работы фильтра в предыдущий момент времени, ну и конечно входного сигнала.
Давайте попробуем посчитать этот фильтр в момент времени n и от заданного входного сигнала x, как говорится, в лоб. На языке С++. Я специально разделил пробелами код, на следующем слайде мы увидим зачем. Но, я надеюсь, понятно — функция рекурсивно считает выходной сигнал в предыдущие моменты времени и затем делает вычисления с входным вектором и заданным параметрами a и b по вышесказанной формуле. Давайте попробуем футуризовать ее.
Для этого всего лишь нужно заменить ретурн тайп на future от него, локальные переменные сделать future, rvalue 0 типа дабла заменить на make_ready_future от этого rvalue, предыдущие значения давайте тоже считать асинхронно и к результату аттачить континуэйшн — функцию, которая будет считать саму формулу, зависящую от входного сигнала и коэффициентов. Все понятно здесь? Обратите внимание, что .get() функция в континуэйшне не блокирует и не саспендит тред, потому что .then дает гарантию, что вызовет континуэйшн тогда, когда future станет готовым, то есть либо вычислен результат либо будет холдить экспешн_поинтер, В последнем случае .get сам вызовет эксепшн.
Давайте рассмотрим пример чуть-чуть посложнее с узкочастотным фильтром, который будет пропускать определенные частоты с заданной шириной полосы. Для такой частоты 0.2 Герца и ширины полосы тридцать три тысячных я посчитал коэффициенты a и b. Обратите внимание, что формула фильтра теперь зависит не только от предыдущего значение выходного сигнала, но и от предпредыдущего значения, т.е. представляет из себя чуть более сложное рекуррентное соотношение.
Давайте снова попробуем посчитать функцию в лоб. Все похоже, только теперь мы еще раз рекурсивно считаем значение выходного сигнала в момент времени n-2. Пример на самом деле довольно косметический и неоптимизированный, т.к. мы не используем дополнительную память для того чтобы сохранять результаты предыдущих вычислений выходного сигнала y[n-1] и т.д. Но хорошо показывает идею. Давайте теперь футуризуем данную функцию.
Для этого мы меняем все подобным образом, но я решил оставить немного работы каждому треду, чтобы тот прошелся сам по одной ветви рекурсии, а другую исполнил асинхронно. Обратите внимание, что теперь мы не можем просто аттачить континуэйшн одному future, теперь нам нужно сначала скомпозицировать их параллельно с помощью функции when_all и аттачить континуэйшн к ней. То есть последняя строчка говорит, когда те два future станут готовыми, приаттачь к ним континуйэшн. А континуйэшн выглядит следующе:
Обратите внимание, что континуэйшн принимает future от tuple от двух futures, которые были переданы аргументами в when_all. Это и есть возвращаемое значение when_all функции. И далее делаем всякую магию по анвраппингу future, получаем результаты из tuple, разворачиваем их и считаем значение по заданной формуле. Но позвольте спросить, почему бы нам просто вместо when_all не запустить асинхронно операцию с помощью async и передать ей future параметры?
Код может выглядеть чуть-чуть поприятней, но кто-нибудь видит небольшую проблему здесь? … Проблема здесь в том, что .get функции могут саспендить тред, в том случае, если future еще не вычислены. Обратите внимание не блокировать, а саспендить, поскольку если future не вычислен, в вызове .get локал скедэлер просто вызовет другой тред из очереди на исполнение. То есть в HPX мы не говорим в терминах блокирования, а говорим в терминах саспэндинга, или приостановки, не знаю, по-русски. И создание нового треда с его последующим саспендингом может являться не совсем оптимальным решением, т.к. выделяется лишнее пространство для стека треда, т.е. засоряется TLB и т.д. Чтобы оптимизировать данную ситуацию HPX обеспечивает механизм, который называется dataflow.
dataflow имеет такой же синтаксис и такую же внешнюю семантику, как и async. Но его преимущество в том, что его вызов гарантируется тогда и только тогда, когда future, которые являются параметрами коллэбл обжекта, переданного а dataflow, будут готовы. Но давайте посмотрим на ситуацию с точки зрения еще нового пропоузала в 17 стандарт.
Мы можем имплементировать такую же семантику с использованием await, при этом сделав код гораздо чище и понятней. Но давайте вернемся к нашей имплементации с dataflow и сделаем замеры последовательной и футуризированной версии для выходного сигнала для n = 35.
Может показаться не совсем ожиданным, но футуризованная версия работает почти в 35 раз медленнее обычной синхронной. И это кажется понятным, т.к.
Давайте теперь введем понятие гранулярности задачи или гранулярности треда, т.е. количество работы, которую тред выполняет в течение своей жизни. Для этого установим некий трешолд или порог, такой что если n опустится ниже него, тред будет выполняться синхронно. Тем самым, регулируя порог, мы можем регулировать время работы одного треда. И давайте взглянем на следующий график.
Здесь ось x - наш порог, от 0 до 35, а ось y в логарифмическом масштабе изображает отношение футуризованной версии алгоритма к последовательной синхронной. Мы видим, что начиная с порога 10 и далее футуризованная версия дает сильный прирост в производительности, далее идет достаточно долгая планка, в течение которой футуризованная версия работает быстрее в более чем 10 раз и затем идет опять вверх, потому что работа начинает все больше и больше напоминать последовательную синхронную.
Давайте теперь рассмотрим мощь HPX на распределенных системах.
Представим, что нам нужно запустить асинхронно функцию calculate на каком-нибудь из нодов в нашей системе. Для начала — как бы это выглядело со стандартной библиотекой C++.
Сначала заменим futures с std нэймспейса на hpx. И данный вариант действительно будет работать, только внутри одного нода.
Затем выпишем макрос HPX_PLAIN_ACTION, где первый параметр — имя функции, а второй параметр название экшна. Экшн — это абстракция HPX над функциями, которые могут быть вызваны удаленно. На самом деле данный макрос просто создает тип-обертку функции calculate, которая может быть сериализована, иметь имя для того, чтобы быть записана в парсель — некий пакет для передачи по сети.
Затем вызовем функцию hpx::find_remote_localities, которая возвращает вектор всех зарегистрированных локальностей, в простейшей абстракции, нодов, и возьмем ее первый элемент. Элемент имеет тип id_type — своего рода указатель поверх всей системы на объекты в глобальном пространстве. Этот id_type в данном случае является адресом нода, на котором мы хотим вызвать данный экшн.
И давайте теперь внутри async инстанцируем временный объект типа calculate_action и передадим первый параметр в него — адрес локалити, на котором хотим асинхронно вызвать этот экшн.
Теперь графически представим, как это работает. Пусть серые треды — кернел треды и каждая локалити имеет 5 единиц вычисления, на каждой из которых выполняется кернел тред. Представим, что красная штука — это HPX тред, который замаппен на этот кернел тред. Этот HPX тред вызывает асинхронно экшн на второй локалити, та его исполняет, в то время тот кернел тред исполняет другую задачу, и, когда результат готов, и 2 локалити вернула результат, кернел тред продолжает исполнение того HPX треда. На самом деле продолжить исполнение мог любой другой кернел тред, своровав ее из очереди первого кернел треда.
Давайте теперь представим, что хотим иметь темплейтный экшн, а именно функцию Бесселя из библиотеки Boost.Math, зависящую от двух темплейтных параметров, непосредственно типов ее аргументов.
Для этого, для удобства, войдем в то же пространство имен, и определим экшн вручную, без использования макроса HPX_PLAIN_ACTION. Для этого унаследуем его от hpx::actions::make_action, у которого первый параметр - тип оборачиваемой функции, второй параметр, ее адрес, а третий — сам экшн, посольку make_action является CRTP классом.
Внутри мэйна инстанцируем объект нашего экшна, и с помощью функции hpx::find_all_localities() вызовем экшн на всех локалитях, сохранив shared_state результатов в векторе из futures
На данной таблице изображены синтаксисы вызовов функций и экшнов в C++ и HPX. Так, сам язык предоставляет нам синхронный вызов функций, стандартная библиотека предоставляет асинхронный с помощью async, а HPX предоставляет остальное — а именно синхронный и асинхронный вызовы экшнов, а также вызов функций и экшнов с семантикой fire & forget, то есть запустить и забыть, без надобности в результате.
Представьте ситуацию, когда вы хотите создать объект C++ класса удаленно на другой машине и затем асинхронно вызвать метод этого класса. Как бы Вы это сделали c HPX? Давайте посмотрим. Пусть мы хотим создать объект типа remote_object на специфицированной локалити и вызвать асинхронно функцию apply_call на ней.
В HPX все классы, которые могут быть использованы удаленно, называются компоненты. Так, первое, что Вам нужно сделать, это создать компонент, представляющий данный класс. В этом случае пусть это будет remote_object_component, унаследовать его от определенного CRTP класса simple_component_base. Определим внутри него метод call, и определим component action внутри него, с помощью макроса HPX_DEFINE_COMPONENT_ACTION
Далее зарегистрируем компонент с помощью макроса HPX_REGISTER_COMPONENT и зарегистрируем HPX_REGISTER_ACTION. Сделать это нужно в глобальном пространстве имен, потому что макросы раскрывают hpx-ные нйэмспейсы, которые частично или полностью специализирует классы и трейты и делают еще всякие разные штуки.
Далее, внутри мейна мы легко можем создать объект на какой-либо локалити и с помощью функции hpx::new_, которая асинхронно создаст объект на специфицированной локалити. Асинхронное создание полезно, потому что создание компонента может быть долгой операцией. и далее с помощью функции apply, которая реализует fire&forget семантику, вызываем экшн на данном объекте. Обратите внимание, что теперь параметром в эпплай является id_type представляющей не локалити, а id удаленного объекта. Довольно удобно на мой взгляд. Но проблема данного подхода в том, что id_type это какой поинтер подобный указателю на void, стирающий информацию о настоящем типе remote_object_component.
Для этого подхода в HPX существует клиенты — простые классы, которые холдят id компонента, на который ссылаются, и наследует немного API от базового класса client_base. Ничего замысловатого. Мы сами определяем функцию apply_call, которая также асинхронно зовет метод call удаленного компонента.
И теперь main выглядит еще проще.
Это небольшая картинка представляет предыдущий пример, как это работает на двух локалитях, где component создан в глобальном адресном пространстве на второй локалити, а локалити 1 ссылается на него с помощью объекта клиента.
Следующий код создает компоненты на всех локалятях и вызывает метод call на них.
Так это выглядит в последнем случае.
HPX с распределенной точки зрения представляет из себя систему, где ноды содержат компоненты, на которых можно ссылаться из других нодов в приватном адресном пространстве. Также, так как HPX реализует AGAS — активное глобальное адресное пространство, преимуществом модели является одинаковый доступ к компонентам вне зависимости, локально они размещены или удаленно, а также возможность переносить компоненты на другие локалити, без нужды обновления ссылок на них.
И последнее, о чем я бы с Вами хотел поговорить — это техникал спецификэйшн для параллелизма в новый стандарт. Этот документ описывает алгоритмы, расширяющие и дополняющие давно полюбившиеся нам последовательные алгоритмы из STL. Здесь я привел непосредственно картинку-список всех алгоритмов из техникал спецификэйшна,
В HPX эти алгоритмы уже разрабывается более чем год и реализовано уже более 80 процентов, насколько я знаю. Нам они нравятся, так как они реализуют более высокую абстракцию над async и dataflow.
Окей, главное отличие данных алгоритмов от их последовательных друзей в том, что теперь они применяют параметр описывающий политику для алгоритма.
Класс sequential_execution_policy требует последовательное вычисление алгоритма, parallel_execution_policy говорит, что алгоритм может быть распараллелен, а parallel_vector_execution_policy указывает что исполнение алгоритма может быть векторизовано и распараллелено.
В HPX мы опять пошли дальше и реализовали асинхронную семантику для алгоритмов, так как fork-join природа данных алгоритмов не подходит для асинхронных задач. Мы добавили политику sequential_task_execution_policy, говорящую, вычисли алгоритм последовательно но асинхронно и верни мне future на void или результат алгоритма, а parallel_task_exection_policy соответсвенно можешь вычислить алгоритм параллельно, но асинхронно верни мне future.
Давайте рассмотрим простой пример алгоритма мап-редюс, любимый всякими функциональными парнями. Алгоритм в сушности преобразует входной набор данных и редюсит результат промежуточной последовательности. Как бы вы его реализовали в обычном C++ со стандартной библиотекой? …
Да, довольно просто и обычно, с привычными функциями transform и accumulate. Теперь давайте рассмотрим, как можно его футуризовать и распараллелить.
Для этого вызовем параллельную-таск версию transform и присоединим к ней континуэйшн. Обратите внимание, что нам нужно сохранять время жизни объектов, использующихся в асинхронных операциях. Помните, так же, как в буст асио? Поэтому внутри лямба функции континуэшна shared_ptr на вектор захватывается и ретэйнится. Это неплохо. Но в техникал спецификэйшн решили, что данная функцию довольно часто используется и для того, чтобы не городить слежку за лайфтаймом, transform_reduce уже сделана за нас.
Окей, и знаете, это последний слайд моей презентации.
Всем большое спасибо за внимание и не забудьте прочекать HPX!