On 4/12/23 18:53, Edward Catmur wrote:
> On Mon, 10 Apr 2023, 08:57 Andrey Semashev via Std-Discussion,
> <std-discussion@lists.isocpp.org
> <mailto:std-discussion@lists.isocpp.org>> 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.