Live data from Hacker News

The Downsides of C++ Coroutines

reductor.dev

21–30 of 51 posts

Re: The Downsides of C++ Coroutines

#21
Eh, I use them and am quite productive with them. Some of the downsides I don't really buy, for example, the argument regarding allocations. In a typical task engine, you're allocating state per task anyways. Sure you could have custom arenas and such, but you can do that with C++ coroutines also by overriding operator new/delete on the promise object. The lifetime concerns are par for the course when it comes to async stuff (assuming that's how you're using C++ coroutines).

Re: The Downsides of C++ Coroutines

#22

Eh, I use them and am quite productive with them. Some of the downsides I don't really buy, for example, the argument regarding allocations. In a typical task engine, you're allocating state per task anyways. Sure you could have custom arenas and such, but you can do that with C++ coroutines also by overriding operator new/delete on the promise object. The lifetime concerns are par for the course when it comes to asy…

Generators, which already exist in the stdlib, is an example where we can see heap elision being useful, but is currently unreliable in C++. There is a paper "Explicit Coroutine Allocation" that will likely solve this in C++26. The Clang IR project will also improve HALO for the future of (Clang) C++ projects.

Re: The Downsides of C++ Coroutines

#24

I do not understand yet (open to explanations!) what is the difference between stackless and stackful coroutines in the fact that stackless should be cheap and even "collapsable" when nested in lifetimes but if it is not the case... stackful is cheaper . Are not stackless supposed to be more performant? In which cases? Yes I know their virality, potential heap allocations, etc.

Two differences:

First, stackful coroutines use the coroutine stack for everything they do. Stackless coroutines can use the normal thread stack for synchronous calls, and that stack can be shared across any number of coroutines. Per-coroutine allocation is only needed for asynchronous calls.

Second, for stackful coroutines you need to allocate the entire stack up front, and usually you have no way of knowing how much stack might be needed, so you need a conservative upper bound. Normal thread stacks have sizes in megabytes. (That doesn't necessarily correspond to actual memory consumption, since the OS will only reserve physical memory as needed, but the physical reservation for a given stack can only grow, not shrink. And even just allocating the virtual space has a cost.) Most of the time you can get away with stacks that are much smaller, only a few kilobytes, but at the cost of potentially crashing when you've consumed too much stack; it's hard to statically analyze maximum stack usage.

Stackless coroutines will, in general, only allocate memory as needed for each coroutine invocation, so not only are you wasting less memory, you don't have to worry about hitting an arbitrary limit. Allocation elision makes things more complicated since, as the blog post notes, you can end up wasting some memory, but compared to stackful coroutines it's peanuts. But they have the downside that heap allocations and deallocations are expensive; plus, splitting a "stack" of nested calls into separate heap allocations, usually far away from each other in memory, is worse for cache locality.

Re: The Downsides of C++ Coroutines

#25
post #6

> Just like a normal function arguments are passed using registers and the stack, coroutines are using the same ABI as previously specified, however the code different is vastly different. > Finally at the end of the function the stack space initially reserved get’s reset to where it was initially when the function first call happens then returns to the caller. This post could use some editing. I'm having to reread e…

It is due a bit of proof reading. There are some readability issues.

That being said, it is a great article. C++ and coroutines is a story that has been going on for a long time, and the result surprised me. In a bad way.

One bit me right from the start. I copied out an example and it crashed, and it turned out (after hours of searching, reading - the compiler and sanitisers sure weren’t any help) that the problem was that I’d inadvertently made a parameter const& (force of habit) and bound a temporary to it.

My answer to this is simply that I choose not to use coroutines. If I can’t force a compilation failure when I do something dumb, that spooks me.

For a feature released in 2020 it has far too many footguns. Ranges was similar when it came to lifetime footguns. It’s just something that makes it hard to take seriously the claims that it is legacy code that is the reason C++ has a bad rap for safety. Coroutines and ranges are modern features that can shoot your foot off if you don’t know the implementation, which is kind of contrary to the point of making a friendly wrapper over it all.

Re: The Downsides of C++ Coroutines

#26
post #24

I do not understand yet (open to explanations!) what is the difference between stackless and stackful coroutines in the fact that stackless should be cheap and even "collapsable" when nested in lifetimes but if it is not the case... stackful is cheaper . Are not stackless supposed to be more performant? In which cases? Yes I know their virality, potential heap allocations, etc.

Two differences: First, stackful coroutines use the coroutine stack for everything they do. Stackless coroutines can use the normal thread stack for synchronous calls, and that stack can be shared across any number of coroutines. Per-coroutine allocation is only needed for asynchronous calls. Second, for stackful coroutines you need to allocate the entire stack up front, and usually you have no way of knowing how muc…

Technically you could manually grow coroutine stacks the same way the kernel does for thread stacks, by mapping on fault and periodically unmapping everything beyond the red zone. But the complexity would be significant and hard to make it efficient without kernel support.

Re: The Downsides of C++ Coroutines

#27
post #14

Earlier quoted context omitted.

I don't understand how it is any more verbose.

Because you have to keep explicitly passing state between each callback, rather than just using the same context (which still has the ability to delete things if needed).

State capture with lambdas is implicit, and only explicit if you want it to be.

Re: The Downsides of C++ Coroutines

#28
post #20
post #3

Experience teaches me that the worst time to use a new design pattern or technique is _right after you learn about it_. The problem in your code base you thought about while learning the pattern was a useful proxy for where it could be applied, but that doesn't mean it's the right fit. Do it in a scratch refactoring, and wait a week or two before you consider merging it. And make sure you are emotionally as ready to…

I agree that you should always be ready and happy to discard or refactor code as needed. Requirements change, your assumptions may be wrong. But in practice I've more often seen the opposite problem, where organizations end up stuck on C++11 for a decade for no technical reason. It's good to explore the new stuff and eventually adopt what you can use.

Better than c++98 which a lot of organizations are still on.

Re: The Downsides of C++ Coroutines

#29
post #6

> Just like a normal function arguments are passed using registers and the stack, coroutines are using the same ABI as previously specified, however the code different is vastly different. > Finally at the end of the function the stack space initially reserved get’s reset to where it was initially when the function first call happens then returns to the caller. This post could use some editing. I'm having to reread e…

It is due a bit of proof reading. There are some readability issues. That being said, it is a great article. C++ and coroutines is a story that has been going on for a long time, and the result surprised me. In a bad way. One bit me right from the start. I copied out an example and it crashed, and it turned out (after hours of searching, reading - the compiler and sanitisers sure weren’t any help) that the problem wa…

The compiler won't stop you storing a pointer to a local variable and dereferencing it later, either. That's just the nature of programming without managed memory. Calling a coroutine is essentially the same as temporarily "returning" from the current stack frame, so all of the usual practices around taking pointers apply.

I agree with your conclusion of not using C++ coroutines, though. It seems like the design falls somewhere in the "worst of both worlds". I would rather either use a library that implements coroutines with the minimal amount of C and inline assembly if performance is critical, or some higher level abstraction that works well with all other language features.

Re: The Downsides of C++ Coroutines

#30
post #6

> Just like a normal function arguments are passed using registers and the stack, coroutines are using the same ABI as previously specified, however the code different is vastly different. > Finally at the end of the function the stack space initially reserved get’s reset to where it was initially when the function first call happens then returns to the caller. This post could use some editing. I'm having to reread e…

Seconded. It's quite a nice article, held back by the lack of editing.

I live eat and breathe a few deeply technical things (including c++) but am hampered by lack of proficiency at clear communication and expression. Becoming increasingly self-aware of this, seeing a fine article like this about a topic I understand, "in the wild" only underscores the importance. :)

I can feel myself automatically rewriting it, just like how typos jump out.

I wonder if that would be actually useful (a rewrite) to anyone.

Post reply on HN