Non-allocating future-promise
Dear Boost,
I have been working on non-allocating future-promise which when
combined with proposed expected
I have been working on non-allocating future-promise which when combined with proposed expected
(i.e. basic_promise >) will provide a close substitute to std::future/promise, except that it doesn't use an allocator and can reduce to pure constexpr when possible e.g. this code: __attribute__((noinline)) int test1() { promise<int> p; auto f(p.get_future()); p.set_value(5); return f.get(); }
... reduces to exactly:
_Z5test1v: # @_Z5test1v .cfi_startproc # BB#0: # %_ZN7promiseIiJEED2Ev.exit2 movl $5, %eax ret
That's cool! I assume all of the necessary synchronization is done lock-free?
The non-allocating future-promise is intended to be compatible with the Concurrency TS extensions (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4123.html) where futures can have continuations scheduled against them via a future.then(callable). However, this involves an unavoidable memory allocation, which while fine for dynamically assigned continuations is overkill for statically assigned continuations.
There is no reason why one could not also have a method of statically adding continuations which uses type extension to store the additions. Let us say we have a template
class promise and that this promise also provides a .then(callable) like this:
FWIW, N4123 adds .then to the future<> type, not the promise<>. I don't think it makes a lot of sense to add it to the promise in the first place as continuations are something local to the consumer, not the producer.
template<class F> promise
then(F &&c) { return promise (std::move(*this), std::forward<F>(c)); } ... and therefore to statically add continuations to the future promise, one simply iterates .then(callable) on the promise like this:
auto p=promise<int>().then([]).then([]) ...; auto f=p.get_future(); p.set_value(5); // executes all the continuations
My first question to the Boost community is this: is there some less ugly way? I'm thinking maybe the promise's constructor could take a set of continuations, and then a make_promise(callable, callable, ...) could construct a statically continued future-promise?
You seldom want to chain .then() calls, in my experience. Most of the time to attach one continuation and pass the resulting future to some other place where you might attach another continuation, etc.
My second question to the Boost community is this: should static continuations be able to see the value being set? Or indeed, to modify it before it gets sent to future.get()?
What would be the point of having a continuation which does not see the value once set? Regards Hartmut --------------- http://boost-spirit.com http://stellar.cct.lsu.edu
On 17 Oct 2014 at 16:07, Hartmut Kaiser wrote:
__attribute__((noinline)) int test1() { promise<int> p; auto f(p.get_future()); p.set_value(5); return f.get(); }
... reduces to exactly:
_Z5test1v: # @_Z5test1v .cfi_startproc # BB#0: # %_ZN7promiseIiJEED2Ev.exit2 movl $5, %eax ret
That's cool! I assume all of the necessary synchronization is done lock-free?
Yep. Using my new spinlock library, the one I built the safe erasing concurrent_unordered_map with. I can absolutely guarantee that this sequence always reduces to nothing: basic_promise<int> p; p.set_value(5); auto f(p.get_future()); return f.get(); This is because we can detach the future from the promise in get_future() and therefore skip use of atomics, and it turns out that clang and GCC are clever here and spot the opportunity to reduce. I am currently deciding what to do about this sequence: basic_promise<int> p; auto f(p.get_future()); p.set_value(5); return f.get(); The big problem is that you can't have non-trivial destructors with constexpr which rules that out (we need destructors to zero out the pointer to self in the other side). We cannot switch on atomic use if we wish to retain constexpr reduction, so the default safe choice is: 1. Switch on atomic use in get_future(), so get_future() halts constexpr onwards. 2. We add an atomic_basic_future which turns on atomics like this: basic_promise<int> p; auto f(make_atomic(p.get_future())); // switches on locking // Locks promise, locks future, sets value, detaches relationship, unlocks p.set_value(5); // constexpr return f.get(); The future<T> convenience template alias would therefore always alias to atomic_basic_future<> for compatibility.
The non-allocating future-promise is intended to be compatible with the Concurrency TS extensions (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4123.html) where futures can have continuations scheduled against them via a future.then(callable). However, this involves an unavoidable memory allocation, which while fine for dynamically assigned continuations is overkill for statically assigned continuations.
There is no reason why one could not also have a method of statically adding continuations which uses type extension to store the additions. Let us say we have a template
class promise and that this promise also provides a .then(callable) like this: FWIW, N4123 adds .then to the future<> type, not the promise<>.
Sure. That's because future.then() executes its continuations at the point of someone doing a future.get(). As promise.then() attaches continuations to the promise instead, it would execute its continuations at the point of someone doing a promise.set_value() or promise.set_exception(). (Some might wonder why not subclass basic_promise<> and override the set_value()/set_exception() with custom behaviour? Unlike promise, basic_promise is designed to be customised and subclassed in a family of future-ish promise-ish types. It's a very valid point, but I worry about generic extension, see below).
I don't think it makes a lot of sense to add it to the promise in the first place as continuations are something local to the consumer, not the producer.
There is a very valid use case for continuations being executed at both places: at the point of value release AND/OR at the point of value acquire. AFIO, for example, schedules its continuations in the thread where an op completes, not in the thread which gets the result of an op (which may be never). I currently have a struct async_io_op containing a shared_future and a routine which wraps set_value() to launch the continuations, I would much rather prefer a native afio::future<> with customised behaviour. The really key part is that all futures belonging to the future family can work together. If I therefore feed to when_all() a heterogenous sequence of future family types, that "just works" because every future turns into a permit object fundamentally. Right now one has to hoop jump a bit mixing when_all() with AFIO because AFIO needs to store extra stuff with each future, hence the struct async_io_op wrapping the future when ideally we'd have the future wrap the extra stuff. However, it is exactly implementing things like when_all() where being able to statically hook arbitrary continuations to either side of the future-promise relationship *from the outside* can make life vastly easier. One simply needs to accept that your basic_future<...> may contain a very long sequence of variadic lambda types, and therefore you can only ever use auto or decltype/result_of for its storage declaration. Obviously if you don't use static continuations, one need not worry about unwritable type declarators.
template<class F> promise
then(F &&c) { return promise (std::move(*this), std::forward<F>(c)); } ... and therefore to statically add continuations to the future promise, one simply iterates .then(callable) on the promise like this:
auto p=promise<int>().then([]).then([]) ...; auto f=p.get_future(); p.set_value(5); // executes all the continuations
My first question to the Boost community is this: is there some less ugly way? I'm thinking maybe the promise's constructor could take a set of continuations, and then a make_promise(callable, callable, ...) could construct a statically continued future-promise?
You seldom want to chain .then() calls, in my experience.
You're right for non-generic code. The trouble with families of futures is that the metaprogramming can explode. I don't want bad design choices now to break the genericity later.
Most of the time to attach one continuation and pass the resulting future to some other place where you might attach another continuation, etc.
I have no issue with this and dynamic continuations.
With static continuations I can't see how to do static continuations
without either (a) elaborating the type of both future and promise to
store those continuations or (b) type slice the pointer from promise
to future, and have a virtual function be invoked to set values.
The (a) scenario forces the programmer to add all static
continuations to promise before fetching a future.
The (b) scenario is a big vote in favour of an atomic_basic_future<>
which turns on atomics and contains a virtual type uneraser function
for promise to set the value through. It would all still be no alloc
and still very, very fast - we are talking dozens of opcodes versus
thousands with std::future.
Some might wonder why do we need constexpr reduction of future
promise anyway? Well, monadically speaking, a promise suspends
execution and a future resumes execution. When combined with
expected
My second question to the Boost community is this: should static continuations be able to see the value being set? Or indeed, to modify it before it gets sent to future.get()?
What would be the point of having a continuation which does not see the value once set?
I was thinking that basic_future and basic_promise wouldn't force any
particular continuations policy on subclasses - basic_future<void> is
a good example why.
I was thinking that basic_future<T>.then(callable) would do the
following outcomes:
1. If callable(basic_future<T>), the future just set/got is passed to
the continuation. As future<U> is template aliased to
basic_future
Niall,
__attribute__((noinline)) int test1() { promise<int> p; auto f(p.get_future()); p.set_value(5); return f.get(); }
... reduces to exactly:
_Z5test1v: # @_Z5test1v .cfi_startproc # BB#0: # %_ZN7promiseIiJEED2Ev.exit2 movl $5, %eax ret
That's cool! I assume all of the necessary synchronization is done lock-free?
Yep. Using my new spinlock library, the one I built the safe erasing concurrent_unordered_map with.
What suspension mechanism does this rely on? Does it suspend the kernel thread?
I can absolutely guarantee that this sequence always reduces to nothing:
basic_promise<int> p; p.set_value(5); auto f(p.get_future()); return f.get();
This is because we can detach the future from the promise in get_future() and therefore skip use of atomics, and it turns out that clang and GCC are clever here and spot the opportunity to reduce. I am currently deciding what to do about this sequence:
basic_promise<int> p; auto f(p.get_future()); p.set_value(5); return f.get();
The big problem is that you can't have non-trivial destructors with constexpr which rules that out (we need destructors to zero out the pointer to self in the other side). We cannot switch on atomic use if we wish to retain constexpr reduction, so the default safe choice is:
1. Switch on atomic use in get_future(), so get_future() halts constexpr onwards.
2. We add an atomic_basic_future which turns on atomics like this:
basic_promise<int> p; auto f(make_atomic(p.get_future())); // switches on locking // Locks promise, locks future, sets value, detaches relationship, unlocks p.set_value(5); // constexpr return f.get();
The future<T> convenience template alias would therefore always alias to atomic_basic_future<> for compatibility.
While all of this demonstrates impressive optimization technologies, I don't think it's a relevant use case. In 99.9% of all use cases this is not applicable. So why worry?
The non-allocating future-promise is intended to be compatible with the Concurrency TS extensions (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4123.html) where futures can have continuations scheduled against them via a future.then(callable). However, this involves an unavoidable memory allocation, which while fine for dynamically assigned continuations is overkill for statically assigned continuations.
There is no reason why one could not also have a method of statically adding continuations which uses type extension to store the additions. Let us say we have a template
class promise and that this promise also provides a .then(callable) like this: FWIW, N4123 adds .then to the future<> type, not the promise<>.
Sure. That's because future.then() executes its continuations at the point of someone doing a future.get().
No, future.then() is called whenever somebody else sets the value (using promise.set_value()) - well except when using launch::deferred, but there all bets are off anyways.
As promise.then() attaches continuations to the promise instead, it would execute its continuations at the point of someone doing a promise.set_value() or promise.set_exception().
(Some might wonder why not subclass basic_promise<> and override the set_value()/set_exception() with custom behaviour? Unlike promise, basic_promise is designed to be customised and subclassed in a family of future-ish promise-ish types. It's a very valid point, but I worry about generic extension, see below).
I'd be absolutely against attaching continuations through the promise. It's conceptually wrong.
I don't think it makes a lot of sense to add it to the promise in the first place as continuations are something local to the consumer, not the producer.
There is a very valid use case for continuations being executed at both places: at the point of value release AND/OR at the point of value acquire. AFIO, for example, schedules its continuations in the thread where an op completes, not in the thread which gets the result of an op (which may be never). I currently have a struct async_io_op containing a shared_future and a routine which wraps set_value() to launch the continuations, I would much rather prefer a native afio::future<> with customised behaviour.
The really key part is that all futures belonging to the future family can work together. If I therefore feed to when_all() a heterogenous sequence of future family types, that "just works" because every future turns into a permit object fundamentally. Right now one has to hoop jump a bit mixing when_all() with AFIO because AFIO needs to store extra stuff with each future, hence the struct async_io_op wrapping the future when ideally we'd have the future wrap the extra stuff.
However, it is exactly implementing things like when_all() where being able to statically hook arbitrary continuations to either side of the future-promise relationship *from the outside* can make life vastly easier. One simply needs to accept that your basic_future<...> may contain a very long sequence of variadic lambda types, and therefore you can only ever use auto or decltype/result_of for its storage declaration.
Obviously if you don't use static continuations, one need not worry about unwritable type declarators.
Sorry, you lost me here.
template<class F> promise
then(F &&c) { return promise (std::move(*this), std::forward<F>(c)); } ... and therefore to statically add continuations to the future promise, one simply iterates .then(callable) on the promise like this:
auto p=promise<int>().then([]).then([]) ...; auto f=p.get_future(); p.set_value(5); // executes all the continuations
My first question to the Boost community is this: is there some less ugly way? I'm thinking maybe the promise's constructor could take a set of continuations, and then a make_promise(callable, callable, ...) could construct a statically continued future-promise?
You seldom want to chain .then() calls, in my experience.
You're right for non-generic code. The trouble with families of futures is that the metaprogramming can explode. I don't want bad design choices now to break the genericity later.
I have not seen this happening even for generic code for our use cases, but I probably don't understand yours.
Most of the time to attach one continuation and pass the resulting future to some other place where you might attach another continuation, etc.
I have no issue with this and dynamic continuations.
With static continuations I can't see how to do static continuations without either (a) elaborating the type of both future and promise to store those continuations or (b) type slice the pointer from promise to future, and have a virtual function be invoked to set values.
The (a) scenario forces the programmer to add all static continuations to promise before fetching a future.
The (b) scenario is a big vote in favour of an atomic_basic_future<> which turns on atomics and contains a virtual type uneraser function for promise to set the value through. It would all still be no alloc and still very, very fast - we are talking dozens of opcodes versus thousands with std::future.
What do you mean by 'static continuation'? What's the difference to a continuation as defined in N4123?
Some might wonder why do we need constexpr reduction of future promise anyway? Well, monadically speaking, a promise suspends execution and a future resumes execution. When combined with expected
, you effectively get coroutined monadic programming via future promise because you can squirrel away a monadic operation into a promise, and resume execution from a future. This isn't as good as C++ 17 resumable functions. But it may be handy enough, and we get this now not later.
My second question to the Boost community is this: should static continuations be able to see the value being set? Or indeed, to modify it before it gets sent to future.get()?
What would be the point of having a continuation which does not see the value once set?
I was thinking that basic_future and basic_promise wouldn't force any particular continuations policy on subclasses - basic_future<void> is a good example why.
I was thinking that basic_future<T>.then(callable) would do the following outcomes:
1. If callable(basic_future<T>), the future just set/got is passed to the continuation. As future<U> is template aliased to basic_future
> this matches the then() in the Concurrency TS. 2. If callable(T) i.e. the type carried by the basic_future, this is equivalent to calling the callable with the future.value(). If T were expected, that means one resumes a monadic sequence.
3. If callable(), the callable is simply invoked. It costs me nothing to add this, so you might as well. I can see atexit_future() type use cases.
Sure. Regards Hartmut --------------- http://boost-spirit.com http://stellar.cct.lsu.edu
On 17 Oct 2014 at 19:42, Hartmut Kaiser wrote:
Yep. Using my new spinlock library, the one I built the safe erasing concurrent_unordered_map with.
What suspension mechanism does this rely on? Does it suspend the kernel thread?
Nothing currently, but it will use my failed proposed C11/pthreads permit object. Myself and Hans Boehm spent an awful lot of time back in the day getting that permit object to behave well under all sorts of weird load conditions. It's also portable, and is safe to destroy concurrent to signalling. It has a few other useful features too which makes it ideal as the standard kernel wait abstraction. I'll be keeping around a thread safe process wide cache of unused permit objects, so grabbing one lazily will usually be fast.
I can absolutely guarantee that this sequence always reduces to nothing: ...
While all of this demonstrates impressive optimization technologies, I don't think it's a relevant use case. In 99.9% of all use cases this is not applicable. So why worry?
This is a very valuable statement. It showed me that I was coming at non-allocating future promise all wrong design wise - I, like the present Concurrency TS, was thinking in terms of composing futures into continuations. It has suddenly dawned on me that this is all wrong - I should in fact be thinking in terms of *flow* of continuations, and that future-promise is simply where a river forks, or a vertex appears in a graph. That utterly changes the design. We still get non-allocating future-promise which template aliases a std::future compatible variant. But the design inverts inside out. We now hang future-promise onto thenables, not the other way round. I think I now understand where Louis Dionne was coming from in a private mail when he was trying to explain how Hana might do this stuff. I'm getting old and slow in the mind, but I ain't quite dead yet. I'll see if I can come up with a new prototype next week in time for the Urbana meeting.
FWIW, N4123 adds .then to the future<> type, not the promise<>.
Sure. That's because future.then() executes its continuations at the point of someone doing a future.get().
No, future.then() is called whenever somebody else sets the value (using promise.set_value()) - well except when using launch::deferred, but there all bets are off anyways.
Except launch::async | launch::deferred is the default launch policy for futures created from std::promise or a std::packaged_task, and so therefore it does indeed execute continuations at future.get(). See http://www.open-std.org/jtc1/sc22/WG21/docs/papers/2014/n4107.html, I see the pre-Urbana changes haven't changed this. There is value in both points of launch. I think personally it should be selectable. Anyway, the new design I have in mind makes .then() merely a backwards compatibility shim, the new design starts with everything being a continuation and promise-future is merely a potential transformation rather like a Haskell monadic bind. If you want a continuation to occur promise side and another future side, simply insert the promise-future construct in between the two in the overall sequence. Sorry, I'll hopefully show this in code later next week, I am probably being a bit excited and speaking nonsense.
As promise.then() attaches continuations to the promise instead, it would execute its continuations at the point of someone doing a promise.set_value() or promise.set_exception().
(Some might wonder why not subclass basic_promise<> and override the set_value()/set_exception() with custom behaviour? Unlike promise, basic_promise is designed to be customised and subclassed in a family of future-ish promise-ish types. It's a very valid point, but I worry about generic extension, see below).
I'd be absolutely against attaching continuations through the promise. It's conceptually wrong.
Agreed. Promise suspends a continuations sequence, Future resumes a sequence (optionally on another thread).
What do you mean by 'static continuation'? What's the difference to a continuation as defined in N4123?
Continuations in N4123 are big fat heavy things with an implied memory allocation and a probably std::function wrap. Continuations in N4213 let you do: future<int> &f; if(i_feel_like_it) f.then([]); Which is absolutely fine. However, sometimes you will always do: promise<int> &p; auto f=p.get_future().then([]); // always continue There a memory allocation and std::function wrap is totally unnecessary because the compiler has sufficient information to construct f statically, except that the present design in N4123 requires it. This is what I mean by static continuations - ones which always happen unconditionally. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 19/10/2014 14:18, Niall Douglas wrote:
However, sometimes you will always do:
promise<int> &p; auto f=p.get_future().then([]); // always continue
There a memory allocation and std::function wrap is totally unnecessary because the compiler has sufficient information to construct f statically, except that the present design in N4123 requires it.
This is what I mean by static continuations - ones which always happen unconditionally.
I don't think code like the above would actually occur in the wild. (Or at least if it did, I would regard it as a bug until proven otherwise.) There should be no reason to attach a continuation to the future in code that still has the promise in scope -- it can simply execute that code directly itself before setting the promise's value. In "normal" code, the future is returned from a method to outside code that has no access to the promise, and it is that outside code that attaches continuations. I don't think that the optimisation you refer to applies in that case.
On 21 Oct 2014 at 18:57, Gavin Lambert wrote:
However, sometimes you will always do:
promise<int> &p; auto f=p.get_future().then([]); // always continue
There a memory allocation and std::function wrap is totally unnecessary because the compiler has sufficient information to construct f statically, except that the present design in N4123 requires it.
This is what I mean by static continuations - ones which always happen unconditionally.
I don't think code like the above would actually occur in the wild. (Or at least if it did, I would regard it as a bug until proven otherwise.)
If you think of heterogeneous sequence libraries such as proposed Boost.Hana or proposed Boost.Expected then static continuations are the norm, not the exception. Moreover, you aren't supposed to know or care what those are given they have unknown type (especially with C++ 14 lambdas which have templated call operators too). Where I had previously gone wrong was that I was thinking of futures as taking concrete known types. This was a mistake. They should also be able to take *unknown* types. Then they can transport heterogeneous sequences across threads too e.g. a tuple of lambda types.
There should be no reason to attach a continuation to the future in code that still has the promise in scope -- it can simply execute that code directly itself before setting the promise's value.
In "normal" code, the future is returned from a method to outside code that has no access to the promise, and it is that outside code that attaches continuations. I don't think that the optimisation you refer to applies in that case.
I am preparing a suite of *primitives* with which one can implement conventional future promise, but also any other kind of future-ish promise-ish object. Libraries are expected to roll their own custom futures, and when_all()/when_any() will still understand and cope with heterogeneous future implementation inputs. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
participants (3)
-
Gavin Lambert
-
Hartmut Kaiser
-
Niall Douglas