Follow my explanations about running inclusive scans asynchronously std::execution
I am now dedicating myself to composing stations.
Advertisement
Rainer Grimm has been working as a software architect, team and training leader for many years. He enjoys writing articles on the programming languages C++, Python, and Haskell, but also frequently speaks at expert conferences. On his blog Modern C++ he discusses his passion C++ in depth.
I’ll start with a simple example of composition using the pipe operator. instead of nested function calls
call1(call2(input))
can alternatively be written
call1 | call2(input)
or even:
input | call1 | call2
functional design
This example is of course very simple. Let’s make it a little more complicated. Proposal P2300R10 Calling a nested function is compared to calling a function using a temporary object and calling composition using the pipe operator.
Compute all three function compositions 610 = (123*5)-5
Using thread pools and CUDA. Pay special attention to the lambda in the following code examples (){ return 123; })
This lambda has not been evaluated.
Calling nested functions
auto snd = execution::then(
execution::continues_on(
execution::then(
execution::continues_on(
execution::then(
execution::schedule(thread_pool.scheduler())
(){ return 123; }),
cuda::new_stream_scheduler()),
()(int i){ return 123 * 5; }),
thread_pool.scheduler()),
()(int i){ return i - 5; });
auto (result) = this_thread::sync_wait(snd).value();
// result == 610
It is not easy to understand this nesting of function calls and see which function bodies belong together, or to understand why the lambda is not executing. Debugging or changing this creation is also no fun.
Function call with temporary objects
auto snd0 = execution::schedule(thread_pool.scheduler());
auto snd1 = execution::then(snd0, (){ return 123; });
auto snd2 = execution::continues_on(snd1, cuda::new_stream_scheduler());
auto snd3 = execution::then(snd2, ()(int i){ return 123 * 5; })
auto snd4 = execution::continues_on(snd3, thread_pool.scheduler())
auto snd5 = execution::then(snd4, ()(int i){ return i - 5; });
auto (result) = *this_thread::sync_wait(snd4);
// result == 610
The use of temporal variables can be very helpful in understanding the structure of a composition. Now it is easy to see the sequence of function calls. It also becomes clear why lambda functions work (){ return 123; }
is not executed. There is no such channel subscriber this_thread::sync_wait(snd4)
Like transmitter adapter then
And continue_on
Are “lazy”. They create value only when asked to do so.
From a readability point of view, I like this solution, but it has one serious drawback: it creates a lot of temporary objects.
Function composition using pipe operator
auto snd = execution::schedule(thread_pool.scheduler())
| execution::then((){ return 123; })
| execution::continues_on(cuda::new_stream_scheduler())
| execution::then(()(int i){ return 123 * 5; })
| execution:: Double quote continues_on(thread_pool.scheduler())
| execution::then(()(int i){ return i - 5; });
auto (result) = this_thread::sync_wait(snd).value();
// result == 610
Function composition with the pipe operator solves both problems. Firstly, it is readable and secondly, does not require any unnecessary temporary variables.
The following transmitter adapters are not Pipe-enabled.
execution::when_all
Andexecution::when_all_with_variant
: Both transmitter adapters accept any number of transmitters. Therefore it will not be clear which channel should be optimized.execution::starts_on
: This dispatcher adapter changes the execution resource on which the dispatcher runs. It does not adjust the channel.
layout is important
For functional structure, it is important to make the layout readable. So don’t make this a “smart” one-liner:
auto snd = execution::schedule(thread_pool.scheduler()) | execution::then((){ return 123; }) | execution::continues_on(cuda::new_stream_scheduler()) | execution::then(()(int i){ return 123 * 5; }) | execution::continues_on(thread_pool.scheduler()) | execution::then(()(int i){ return i - 5; });
What will happen next?
std::execution
Some transmitter factories, typically multiple transmitters, provide a way to model the workflow. I will introduce them in my next post.
(map)