Live data from Hacker News

My tutorial and take on C++20 coroutines

scs.stanford.edu

131–140 of 144 posts

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

#131
post #18

Part of this is that I’m tired, but it blows my mind how difficult C++ coroutines are as someone who considers themselves decent at C++ (although maybe I’m not) and uses coroutines in other languages. The amount of code needed to do almost nothing is extraordinary, and putting it all together doesn’t seem like you would often get on your first try. I get that new keywords basically can’t be added, but man, that’s pai…

A part of the tis that the standards committee decided to do a multi phase rollout. They created the core language parts and will hopefully in a future version add the library support making it easier to approach. (And then hopefully find that the design works with the library design ...)

Well put. I wish more features were rolled out as core language features before putting them in std. making things language features makes them possible. Making them library features makes them vocabulary. I’m more eager for things to be possible than I am for them to be ergonomic.

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

#132

Earlier quoted context omitted.

I would say, yes. C++11 is, in some ways, a totally new and more powerful language. I was sold on it as soon as I started learning. C++14 and C++17 add some useful features. I know basically nothing about C++20.

As a non-C++ professional programmer question, what's the occurrence of the codebases you work on, where the standard is significantly/fundamentally C++ 14/17?

We use C++17 to make our professional 3D printers go. We look forward to C++20 features when they become available. We shed a few hundred lines of code when we upgraded to 17 because we could drop hand-rolled stuff. It works. Nothing is meaningfully faster. And if you try hard it’s faster still.

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

#133

Earlier quoted context omitted.

Isn't it like that is most languages? I'm thinking about Python, C#, JS. If you call a blocking function from an async function, you cannot yield from deep inside the blocking function. Why is this a big deal in C++? Am I missing anything that makes c++ coroutines less powerful than other mainstream solutions? Or are people comparing its power with e.g. Lisp or go?

It's a big deal because, while it has some downsides, being stalkless means they can have next to no overhead, meaning it can be performant to use coroutines to write asynchronous code for even very fast operations. The example given https://www.youtube.com/watch?v=j9tlJAqMV7U&t=13m30s is that you can launch multiple coroutines to issue prefetch instructions and process the fetched data, so you can have clean code th…

Wow, that talk is a fantastic link. He actually gets negative overhead from using coroutines, because the compiler has more freedom to optimize when humans don't prematurely break the logic into multiple functions.

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

#134

Earlier quoted context omitted.

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 compiler library interface - where Rust uses Generator, GeneratorState, Pin, etc. Is there something fundamentally different about the design here?

Rust's compiler/library interface is still much simpler than C++'s. An `async fn` call gives you an anonymous-typed object (much like a lambda) which implements the `Future` trait, which has a single method `poll`.

`Generator` and `GeneratorState` are not exposed or usable. There are no knobs to turn like C++'s `await_ready` or `early_suspend`/`final_suspend`. There is no implicit heap allocation or corresponding elision optimization, and thus no need to map between `coroutine_handle`s and promise objects.

To be fair, C++'s design is a bit more flexible in that it supports passing data in and out of a coroutine. But even if you look at Rust's (unstable work-in-progress) approach to supporting this, the compiler/library interface is still way simpler. The difference is really not related to how much functionality is stabilized, but how scattered and ad-hoc the C++ interface is.

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

#135
post #97
post #93

Earlier quoted context omitted.

> And from what I've read they are better than Rusts coroutines for this use case Reference please? In what sense are they better, and what makes them better?

Rust's Futures don't have asynchronous destructors (I don't know if coroutines do). When a Future is aborted early, it's destroyed immediately with no remorse. This means it can't simply offer its own buffers to the kernel, because the kernel could write back after the Future has been freed. An API contract that includes "just be careful not to do the stupid thing" is not good enough by Rust's standards, so the only…

C++ coroutines for async functions (returning std::task) seem to have completion semantics (are not randomly interruptible). See eg the APIs in cppcoro.

However that is not a general property of c++ coroutines. The generator style coroutines also seem randomly cancellable

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

#136

Earlier quoted context omitted.

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 compiler library interface - where Rust uses Generator, GeneratorState, Pin, etc. Is there something fundamentally different about the design here?

In Rust, the state machines just implement one trait, Future, which has one method: poll.

For a very long time, async/await were just normal Rust macros; there was no compilerlibrary interface.

For a year or so, async/await are proper keywords, which provides nicer syntax, and some optimizations that were hard to do with macros (e.g. better layout optimizations for the state machines).

But that's essentially the whole thing.

Looking at safety, flexibility, performance and simplicity, Rust design picks maximum safety, performance, and simplicity, trading off some flexibility in places where it really isn't necessary:

- you can't move a coroutine while its being polled, which is something you probably shouldn't be doing anyways

- you can't control the layout of the coroutine state for auto-generated corotuines; but you can lay them out manually if you need to, for perf (the compiler just won't help you here)

- you need to manually lay out a coroutine and commit to the layout for using them in ABIs

C++ just picks a different design. 100% safety isn't attainable, maximum flexibility is very important, performance is important, but if you need this you have alternatives (callbacks, etc.). I personally just find the API surface of C++ coroutines (futures, promises, tasks, handles, ...) to just be really big.

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

#137
post #123

Earlier quoted context omitted.

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 wh…

oh, yes, in rust coroutines do not normally allocate as far as I understand. This is not the case in C++ unfortunately. This was extremely contentious to say the least, but all alternative designs were either very unsafe or were presented very late, so the committee has preferred to go with something working now instead of something perfect in an indeterminate future.

There are already proposals to improve on the design, but we will have to see if they work out.

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

#138
post #121

Earlier quoted context omitted.

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.

it is not an implementation detail at all. The syntax and semantics are very different. Stackfull coroutines allow suspending more than one activation frame (i.e. a subset of the callstack) in one go. You can of course "emulate" it with stack less coroutines by converting each frame in the call stack to a coroutine, but it is a manual and intrusive process.

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

#139
post #84

Earlier quoted context omitted.

Doing things right takes time. This is one step in making co-routines useful. Library authors now have something to work with to figure out what the next part of the C++ standard is. Expect standard library support in C++ in either c++ 23 or 26.

I agree with GP, I feel a similar disdain when I hear that features that are merged into the standard are for the "library implementors". Boost.Coroutine and Boost.Asio have been around for how long? At least a decade? I think there has been more than enough experience with coroutines and async programming to get coroutines in standard C++ done so that at least the "average" C++ programmer can grok them.

The problem is that it took forever to standardize the internal language-level machinery that the committee run out of time to standardize the user level library bits. Instead of delaying coroutines again (they were originally scheduled to be part of C++17, but then they were taken out of the standard), they decided to merge what they had into the standard.

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

#140
post #115
post #108

Earlier quoted context omitted.

This is how I view iterators, but with an extra notch in difficulty. You really need to understand the coroutine model to get anywhere -- and the same is true of iterators. Are these leaky abstractions? I generally think of leaks as "pushing", but this is "pulling" -- I'd call 'em sucky abstractions. But I digress. I just sat down with the tutorial and banged out what I consider to be a holy grail for quality of life…

So, given sibling comments on stack and function calls, this: skip_iter(int skip, int stop) for(int i=0;i Wouldn't work with complex types, boxed integers and so on? Because calling postfix ++ would be a function/method call?

Stackless coroutines can call other functions just fine (they would completely unusable otherwise). What they can't do is delegate suspension to any called function, i.e. any called function must return before the coroutine can yield as exactly one activation frame (the top level) can be suspended.
Post reply on HN