Live data from Hacker News

My tutorial and take on C++20 coroutines

scs.stanford.edu

121–130 of 144 posts

Re: My tutorial and take on C++20 coroutines

#121
post #19

Some notes: - C++20 coroutines are stackless , meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames . Every coroutine you inv…

I'm trying to wrap my head around what the implication of stackless coroutines is. Can I use them like `yield` in Python+Twisted, i.e. to implement single-threaded, cooperative parallelism? It would not expect to be able to plainly call a function with a regular call, and have that function `yield` for me - but can I await a function, which awaits another, and so on? As far as I understand, C++20 coroutines are just…

> but can I await a function, which awaits another, and so on?

Whether a coroutine is stackful or stackless is largely an implementation detail that has some tradeoffs either way, in either case coroutine semantics can allow you to write efficient asynchronous code imperatively or do your callback-to-async transformation.

Re: My tutorial and take on C++20 coroutines

#122
post #83
post #19

Some notes: - C++20 coroutines are stackless , meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames . Every coroutine you inv…

It seems really dumb that they are stackless. If you are saving/restoring the stack pointer anyway in your yield routine it's trivial to set it to a block of memory you allocated in the initial coroutine creation. Is there no setjmp/longjmp happening? Are C++ 20 coroutines all just compiler slight-of-hand similar to duff's device with no real context switching?

Modern stackful coroutines don't (or shouldn't) use context switching (at least not anything like ucontext_t, just use threads if you're going to spend the cycles to preserve the signal mask) or setjmp/longjmp. Those tricks are slow and hazardous, especially when code needs to cross language boundaries or remain exception safe.

Re: My tutorial and take on C++20 coroutines

#123
post #94

Earlier quoted context omitted.

Does that mean that every nested coroutine (async call) needs another heap allocation, or just the top level one?

What do you mean nested call? As a first approximation, you need an heap allocation for each coroutine function instance for its activation frame. Every time a coroutine instance is suspended, the previously allocated frame is reused. If you instantiate a coroutine from another coroutine, then yes you need to heap allocate again, unless the compiler can somehow merge the activation frames or you have a dedicated allo…

Is that a given, though? Rust's generators are decent prior art - they generate a state machine that would only require heap allocation if the size of the state machine becomes unbounded (for example, a recursive generator). Otherwise the generator is perfectly capable of being stack allocated in its entirety. This turns out to be sufficient for a large amount of programs, with a sufficient workaround for the ones where you can't (box the generator, making the allocation explicit).

Re: My tutorial and take on C++20 coroutines

#124
post #23

Its insane how complex C++20 coroutines are when compared with Rust coroutines.

screw rust (which needs async-std), look at how zig does coroutines.

You don't need async-std for coroutines (generators) in rust (or async at all, they are two different, albeit related features). Async is implemented using generators.

Re: My tutorial and take on C++20 coroutines

#126
post #117
post #19

Some notes: - C++20 coroutines are stackless , meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames . Every coroutine you inv…

Are C++20 coroutines allowed to be recursive? Or does recursing require boxing? For a stackless coroutine the compiler normally has to build a state machine to represent the possible execution contexts, but if the state machine includes itself then it has indeterminate size. Normally you solve this by boxing the state machine at yield points and using dynamic dispatch to call or resume a pending coroutine - which may…

I'm curious. Intuitively, a non-recursive coroutine would yield a state machine that only goes "forward". If you add tail recursion into the mix, you could have cycles in the state machines (going back to the begin state), correct? Of course non-tail recursion would not work within a single frame.

Re: My tutorial and take on C++20 coroutines

#127
post #126
post #117

Earlier quoted context omitted.

Are C++20 coroutines allowed to be recursive? Or does recursing require boxing? For a stackless coroutine the compiler normally has to build a state machine to represent the possible execution contexts, but if the state machine includes itself then it has indeterminate size. Normally you solve this by boxing the state machine at yield points and using dynamic dispatch to call or resume a pending coroutine - which may…

I'm curious. Intuitively, a non-recursive coroutine would yield a state machine that only goes "forward". If you add tail recursion into the mix, you could have cycles in the state machines (going back to the begin state), correct? Of course non-tail recursion would not work within a single frame.

Yes, a tail recursive coroutine could reuse its previous frame context across yields.

With a state machine transform the `resume()` method on a coroutine is a state transformation, it doesn't necessarily know what is "forward" or "backward" in the control flow graph. There are some tricky bits though, since tail recursive functions can have multiple exits but single entries. A recursive coroutine might have multiple exits and multiple entries, so it's not always clear what is "forward" and what is "backward."

Re: My tutorial and take on C++20 coroutines

#128

Earlier quoted context omitted.

Can you say more on this? What is Rust doing differently here that simplifies things?

I find the Rust design very simple: a coroutine is just a state machine, i.e. just a C struct. I find this very easy to reason about. It does not require memory allocations, does not require a run-time, works on embedded targets, etc. Also, the compiler generates all the boilerplate (the state machine) for you, which I find makes it very easy to use. And well, the compiler ensures memory safety, thread safety, etc. w…

I'm not sure that answers my question; C++ also uses a state machine.

Most of the post is concerned with the compilerlibrary interface - where Rust uses Generator, GeneratorState, Pin, etc. Is there something fundamentally different about the design here?

Re: My tutorial and take on C++20 coroutines

#129
post #23

Its insane how complex C++20 coroutines are when compared with Rust coroutines.

Can you say more on this? What is Rust doing differently here that simplifies things?

Rust's interface at the lowest level is just a single method, which in C++ could roughly look like:

   optional poll(*waker_t)
When polled it either returns the final result or that it's pending. If it's pending, it keeps the reference to the waker arg, and uses it to notify when it's ready to be polled again.

This design is very composable, because a Future can trivially forward the poll calls to other futures, and the wakers can be plugged into all kinds of existing callback-based APIs.

async/await is a syntax sugar on top of that that auto-generates the poll methods from async function's bodies (where each poll() call advances state to the next .await point).

There's no built-in runtime in the language that does the polling. You make your own event loop, and that gives you freedom to make it simple, or fancy multithreaded, or deterministic for test environments, etc.

Re: My tutorial and take on C++20 coroutines

#130
post #117
post #19

Some notes: - C++20 coroutines are stackless , meaning that multiple coroutines will share a single OS thread stack. This is non-obvious when you first look in to them them because coroutines look just like functions. The compiler does all the work of ensuring your local variables are captured and allocated as part of the coroutine yield context, but these yield contexts are not stack frames . Every coroutine you inv…

Are C++20 coroutines allowed to be recursive? Or does recursing require boxing? For a stackless coroutine the compiler normally has to build a state machine to represent the possible execution contexts, but if the state machine includes itself then it has indeterminate size. Normally you solve this by boxing the state machine at yield points and using dynamic dispatch to call or resume a pending coroutine - which may…

The state is allowed to be heap-allocated but can be optimized onto the stack. But if it calls recursively without control flowing back out again, then I’d think the nested state could live on the stack so long as the compiler knows the nested state never has to be held across yield/resume.
Post reply on HN