Date: Wed, 12 Apr 2023 23:16:42 -0300
On Wed, 12 Apr 2023 at 15:27, Andrey Semashev via Std-Discussion <
std-discussion_at_[hidden]> wrote:
> On 4/12/23 18:53, Edward Catmur wrote:
> > On Mon, 10 Apr 2023, 08:57 Andrey Semashev via Std-Discussion,
> > <std-discussion_at_[hidden]
> > <mailto:std-discussion_at_[hidden]>> wrote:
> >
> > I don't think that updating a pointer during a context switch would
> be a
> > deal breaker. The co_unhandled_exceptions() mechanism only needs to
> know
> > the current coroutine state of the current thread, it doesn't need a
> > list. If a linked list is needed to be able to return from one
> coroutine
> > to another then such a list must be already in place.
> >
> > Now that I think of it, co_unhandled_exceptions() might not require
> any
> > new pointers. The coroutine already has to keep a pointer to its
> state
> > somewhere to be able to reference its local variables. I don't see
> why
> > co_unhandled_exceptions() couldn't use that pointer.
> >
> > But how can co_unhandled_exceptions() *find* that pointer? The current
> > coroutine state is not stored in per thread memory; it's solely on the
> > stack. (The linked list that's "already in place" is the stack itself.)
>
> The compiler could pass this pointer as a hidden parameter to
> co_unhandled_exceptions() based on implementation-specific attribute, or
> co_unhandled_exceptions() could be a compiler intrinsic to begin with.
> Or, if the pointer is stored in a known fixed location of the parent
> stack frame, co_unhandled_exceptions() could extract it from there. All
> this is to say this task doesn't seem impossible.
>
> > That said, you might be able to find it when unwinding through a
> > coroutine stack frame. I'm still thinking about it, but it seems like it
> > should work. In that case you've only got overhead added to coroutine
> > creation, coroutine footprint, unwinding through a coroutine, and you've
> > doubled the stack footprint and memory access of the success/failure
> > scope guards, compared to a C++17 implementation using
> > unhandled_exceptions only.
>
> Not sure what you mean by doubling the stack footprint and memory access
> of the scope guards. The size of the scope guards didn't change.
>
I may have lost track of things here. Let's go back to your design: you're
adding a co_uncaught_exceptions() counter that's initialized from
uncaught_exceptions() when a coroutine is constructed and stored in
per-coroutine memory. (We'll assume that can be arranged without breaking
ABI, since the coroutine allocation - if there is one - is provided by the
runtime.) The co_uncaught_exceptions() counter is incremented when
unwinding encounters the coroutine, and decremented when a coroutine catch
block is entered - this is fine, unwinding has access to the relevant
information. The scope_success and scope_failure guards record the value of
co_uncaught_exceptions() on construction, and consult it on destruction; if
it is greater than the recorded value (it should never be lesser) then a
failure condition obtains. OK, I can see how there is no increased
footprint, since the recorded co_uncaught_exceptions() value replaces the
recorded uncaught_exceptions() value, my bad.
But *how* do scope_success and scope_failure access the current
coroutine's co_uncaught_exceptions() counter? Obviously it's not a problem
if they're complete automatic objects of the coroutine frame, but
otherwise? e.g. if they're subobjects or dynamically allocated? I'd think
this would require stack walking (non-destructive unwinding), which is
hugely expensive, or constructing a thread-local linked list of coroutines,
which has continuous overhead.
And what for scope_success and scope_failure that aren't constructed from
within coroutines at all? How far do they look to determine to use
uncaught_exceptions() instead?
And what if a dynamically allocated scope_success/scope_failure is
constructed in one coroutine, but then has ownership transferred to another
stack (which may or may not be a coroutine)?
> But doesn't adding the information for co_unhandled_exceptions() to
> > coroutine state break abi?
>
> It should be implementable without breaking the ABI, if the coroutine
> state is allocated and initialized by the runtime (and I assume it
> should be the case). For example, you could put the added counter before
> the data used by the coroutine (parameters and locals), at a negative
> offset known to the runtime.
>
HALO coroutines aren't allocated... but maybe that's OK, since they're
always resumed from the same stack context. (Are they?)
> When I say "the destruction of the scope guard is caused by a stack
> > unwinding procedure" I mean literally that the stack unwinding
> procedure
> > is present higher up in the call stack from the scope guard
> destructor.
> > I'm choosing this criteria because that's what makes most sense to
> me in
> > relation to the expected behavior of the scope guards.
> >
> > That doesn't work. Having an unwind caused destructor above in the stack
> > from a scope guard destructor does not always mean that the scope guard
> > is in a failure state. It feels like we're going round in circles here.
>
> A scope guard destructor would conceal this information by setting the
> uncaught exceptions counter to zero, as described below.
>
Is this a revision to your design, an extension, or an alternate design?
I'm having trouble keeping track.
> > In a
> > > perfect world, I would prefer to have scope guard-like
> > functionality in
> > > the core language (but not in the form of try/finally from
> Java).
> > >
> > > Even if we had scope guard in the core language, its actions would
> be
> > > executed during stack unwinding, no?
> >
> > Yes, but a core language feature could have better syntax and better
> > defined semantics as it wouldn't have to rely on
> unhandled_eceptions().
> >
> > As could a library feature with compiler support, and that wouldn't
> > require new syntax.
>
> A new syntax could offer more benefits, like not having to name the
> scope guard, for example. It would also make the discussion re.
> unhandled_exceptions() moot as it would simply not use it internally.
>
Well, yes. The argument against new syntax has always been that it is
unnecessary since we can implement scope guards in library. If it turns out
that scope_success and scope_failure can't be implemented without either
incorrect behavior, considerable overhead, or heavy restrictions on use,
that might swing things back the other way.
std-discussion_at_[hidden]> wrote:
> On 4/12/23 18:53, Edward Catmur wrote:
> > On Mon, 10 Apr 2023, 08:57 Andrey Semashev via Std-Discussion,
> > <std-discussion_at_[hidden]
> > <mailto:std-discussion_at_[hidden]>> wrote:
> >
> > I don't think that updating a pointer during a context switch would
> be a
> > deal breaker. The co_unhandled_exceptions() mechanism only needs to
> know
> > the current coroutine state of the current thread, it doesn't need a
> > list. If a linked list is needed to be able to return from one
> coroutine
> > to another then such a list must be already in place.
> >
> > Now that I think of it, co_unhandled_exceptions() might not require
> any
> > new pointers. The coroutine already has to keep a pointer to its
> state
> > somewhere to be able to reference its local variables. I don't see
> why
> > co_unhandled_exceptions() couldn't use that pointer.
> >
> > But how can co_unhandled_exceptions() *find* that pointer? The current
> > coroutine state is not stored in per thread memory; it's solely on the
> > stack. (The linked list that's "already in place" is the stack itself.)
>
> The compiler could pass this pointer as a hidden parameter to
> co_unhandled_exceptions() based on implementation-specific attribute, or
> co_unhandled_exceptions() could be a compiler intrinsic to begin with.
> Or, if the pointer is stored in a known fixed location of the parent
> stack frame, co_unhandled_exceptions() could extract it from there. All
> this is to say this task doesn't seem impossible.
>
> > That said, you might be able to find it when unwinding through a
> > coroutine stack frame. I'm still thinking about it, but it seems like it
> > should work. In that case you've only got overhead added to coroutine
> > creation, coroutine footprint, unwinding through a coroutine, and you've
> > doubled the stack footprint and memory access of the success/failure
> > scope guards, compared to a C++17 implementation using
> > unhandled_exceptions only.
>
> Not sure what you mean by doubling the stack footprint and memory access
> of the scope guards. The size of the scope guards didn't change.
>
I may have lost track of things here. Let's go back to your design: you're
adding a co_uncaught_exceptions() counter that's initialized from
uncaught_exceptions() when a coroutine is constructed and stored in
per-coroutine memory. (We'll assume that can be arranged without breaking
ABI, since the coroutine allocation - if there is one - is provided by the
runtime.) The co_uncaught_exceptions() counter is incremented when
unwinding encounters the coroutine, and decremented when a coroutine catch
block is entered - this is fine, unwinding has access to the relevant
information. The scope_success and scope_failure guards record the value of
co_uncaught_exceptions() on construction, and consult it on destruction; if
it is greater than the recorded value (it should never be lesser) then a
failure condition obtains. OK, I can see how there is no increased
footprint, since the recorded co_uncaught_exceptions() value replaces the
recorded uncaught_exceptions() value, my bad.
But *how* do scope_success and scope_failure access the current
coroutine's co_uncaught_exceptions() counter? Obviously it's not a problem
if they're complete automatic objects of the coroutine frame, but
otherwise? e.g. if they're subobjects or dynamically allocated? I'd think
this would require stack walking (non-destructive unwinding), which is
hugely expensive, or constructing a thread-local linked list of coroutines,
which has continuous overhead.
And what for scope_success and scope_failure that aren't constructed from
within coroutines at all? How far do they look to determine to use
uncaught_exceptions() instead?
And what if a dynamically allocated scope_success/scope_failure is
constructed in one coroutine, but then has ownership transferred to another
stack (which may or may not be a coroutine)?
> But doesn't adding the information for co_unhandled_exceptions() to
> > coroutine state break abi?
>
> It should be implementable without breaking the ABI, if the coroutine
> state is allocated and initialized by the runtime (and I assume it
> should be the case). For example, you could put the added counter before
> the data used by the coroutine (parameters and locals), at a negative
> offset known to the runtime.
>
HALO coroutines aren't allocated... but maybe that's OK, since they're
always resumed from the same stack context. (Are they?)
> When I say "the destruction of the scope guard is caused by a stack
> > unwinding procedure" I mean literally that the stack unwinding
> procedure
> > is present higher up in the call stack from the scope guard
> destructor.
> > I'm choosing this criteria because that's what makes most sense to
> me in
> > relation to the expected behavior of the scope guards.
> >
> > That doesn't work. Having an unwind caused destructor above in the stack
> > from a scope guard destructor does not always mean that the scope guard
> > is in a failure state. It feels like we're going round in circles here.
>
> A scope guard destructor would conceal this information by setting the
> uncaught exceptions counter to zero, as described below.
>
Is this a revision to your design, an extension, or an alternate design?
I'm having trouble keeping track.
> > In a
> > > perfect world, I would prefer to have scope guard-like
> > functionality in
> > > the core language (but not in the form of try/finally from
> Java).
> > >
> > > Even if we had scope guard in the core language, its actions would
> be
> > > executed during stack unwinding, no?
> >
> > Yes, but a core language feature could have better syntax and better
> > defined semantics as it wouldn't have to rely on
> unhandled_eceptions().
> >
> > As could a library feature with compiler support, and that wouldn't
> > require new syntax.
>
> A new syntax could offer more benefits, like not having to name the
> scope guard, for example. It would also make the discussion re.
> unhandled_exceptions() moot as it would simply not use it internally.
>
Well, yes. The argument against new syntax has always been that it is
unnecessary since we can implement scope guards in library. If it turns out
that scope_success and scope_failure can't be implemented without either
incorrect behavior, considerable overhead, or heavy restrictions on use,
that might swing things back the other way.
Received on 2023-04-13 02:16:58