[HN Gopher] The Downsides of C++ Coroutines
___________________________________________________________________
The Downsides of C++ Coroutines
Author : msz-g-w
Score : 56 points
Date : 2023-08-11 06:09 UTC (1 days ago)
(HTM) web link (reductor.dev)
(TXT) w3m dump (reductor.dev)
| ninepoints wrote:
| 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).
| Conscat wrote:
| 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.
| shadowgovt wrote:
| Besides that they are yet another feature in the bloated hash
| mess that is c++?
| cshokie wrote:
| This article hits on a number of interesting points. There is a
| lot of complexity to be aware of when using C++ coroutines. And a
| number of "normal" practices become dangerous in them, such as
| pass by reference.
|
| That said, I think they are still very much worth it. Older
| asynchronous programming libraries in C++ are so verbose and so
| much worse than coroutines that it's an obvious choice to use
| coroutines.
|
| Also, there is another hazard that the author does not mention in
| this article: RAII lock wrappers. Holding a lock across
| suspension points is super dangerous. At best it wastes
| performance to leave it locked when blocked. At worst it can
| create deadlocks or corrupt the lock if it is released on a
| different thread than it was acquired.
| rockwotj wrote:
| As someone who writes in C++ and uses coroutines everyday for
| work, I find for our use case this is actually helpful.
|
| We use seastar.io a thread per core framework and locks are
| "async" friendly in that they yield for access instead of
| blocking. Also embracing fully async message passing between
| threads simplifies the programming model a ton.
| mgaunard wrote:
| I don't understand how it is any more verbose.
| nazcan wrote:
| 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).
| meindnoch wrote:
| >RAII lock wrappers
|
| You don't even need coroutines for this to be dangerous.
| Holding locks over callback invocations is a pet peeve of mine
| in PR reviews. Callback invocations, like suspension points,
| can inject arbitrary operations into our code, which can easily
| break prior invariants, yet look innocuous for the casual
| reader.
| commonlisp94 wrote:
| > it's an obvious choice to use coroutines.
|
| I agree, but the other choice is to have traditional threads of
| execution that block. This simple strategy has delivered more
| successful projects than any other.
| mkoubaa wrote:
| 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 discard as you are to land it.
| TillE wrote:
| 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.
| tomcam wrote:
| All true, but another trick is to do extensive web searches to
| see what kind of problems people have had with the new
| approach.
| 7e wrote:
| It's in fashion to dislike fibers, but they're a simpler solution
| that, IMHO, beats coroutines for the wide majority of cases. Even
| threads are a better solution for most cases. Coroutines are like
| the checked exceptions of C++.
| germandiago wrote:
| 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.
| comex wrote:
| 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.
| yakubin wrote:
| > 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 each
| paragraph several times to figure out its intended meaning. Most
| sentences are separate paragraphs with careless mistakes that
| make me feel the author was being chased by someone when writing
| them and couldn't take a breath.
| i-use-nixos-btw wrote:
| 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.
| mgaunard wrote:
| The best solution remains writing asynchronous code in a way that
| is explicitly asynchronous.
|
| Who would have thought?
| sshaw wrote:
| "C++" and "Coroutines". Who would have thought.
|
| But, considering the accelerated releases post C++ 11, I guess
| I'm not surprised.
| pjmlp wrote:
| Anyone that has read "Design and Evolution of C++".
| jupp0r wrote:
| There is nothing inherently asynchronous about coroutines. You
| can use them to model concurrency or even parallelism, but that's
| only a subset of their use cases.
| Calavar wrote:
| What are some of the other use cases?
| spacechild1 wrote:
| Generators come to my mind.
| Jtsummers wrote:
| Many coroutine uses are not asynchronous, but _synchronous_ ,
| they block when resumed and do not execute in parallel. This
| permits cooperative multitasking, versus preemptive (or
| preemptive with a bunch of locks to imitate cooperative which
| is, of course, a waste). Since they can, in principle,
| execute within the same thread (with C++'s implementation and
| some others _you_ the programmer can send them off to other
| threads for execution, but that 's an explicit choice) this
| can simplify concurrent system design and execution (in the
| concurrency is not parallelism sense). In the single threaded
| case, it's also faster than multithreaded asynchronous code
| since the context switching (modulo cache misses) is greatly
| reduced. Especially useful in the case where you want
| synchrony and not asynchrony.
|
| They're also very useful if you've ever had to create a bare
| metal multitasking system. Much easier for state management
| than older style "while (true)" loops with a million state
| variables so functions can resume via a switch/case as
| pseudo-coroutines. (Well, easier if you don't have to
| implement the coroutine mechanism yourself.)
___________________________________________________________________
(page generated 2023-08-12 23:00 UTC)