P0057, continuations, and scope creep

253 views
Skip to first unread message

Nicol Bolas

unread,
Nov 23, 2015, 11:35:23 AM11/23/15
to ISO C++ Standard - Future Proposals
I've been reading through some of the criticisms of P0057 that has been written up in formal proposals. P0158's criticism is interesting, but it occassionally has a tone that seems... petulant, particularly the "nobody implemented it on a compiler that works in Linux, so you don't really know anything" bit. But I found P0162 to be particularly disconcerting, making me wonder what happened with P0057 to bring this about.

As I thought about it, what I feel has happened is that P0057 has succumbed to two problems: trying to do too much, and trying to use the wrong abstraction to do so.

At its core, P0057 is conceptually about one thing: continuations. If you boil down all of the extraneous stuff in the feature, its purpose is to make continuations work more effectively in C++. You take a function, tell it to stop right here, and then you hand the function off to some other code that decides when you get to continue.

This is the "suspend up" model, because the code that decides when you continue is defined ultimately by the expression you are "awaiting" on. The promise machinery in such coroutines exist to allow the caller of them to be able to interact with the coroutine, regardless of where it actually gets finished.

This all ought to make P0057 ideal for continuation-based programming. And the networking TS is all about continuation-based async programming. And yet... P0162 makes it abundantly clear that P0057 kinda sucks at it. That however much the syntax may be nicer, it is no better in performance than doing it manually and in some cases non-trivially worse (percentage-wise).

It is certainly far from being the widely touted "negative overhead abstraction" in these use cases.

How is it that a language feature intended for continuations fails so miserably at working with a continuation-based API?

Well, I think part of the problem is that P0057 is really trying to do too much. If resumable functions are all about continuations, why does `co_yield` exist? After all, `co_yield` doesn't schedule anything; it always suspends the coroutine and returns the given value. The code that's ultimately intended to schedule the resumption is the caller.

In short, `co_yield` is all about turning a suspend-up model into a suspend-down model.

Let's look at some of the things that have to be added to the feature in order to support a suspend-down model.

First, you have to have a way to get rid of the allocation of the coroutine's stack. In normal continuation code, that allocation must happen, because the function is never local to its scope. But with generators, the function is usually local, so the allocation is pointless. This means that P0057 must have specific wording allowing the compiler to elide the stack's allocation. But that's fairly minor.

A more major issue is that promises have to be given access to their coroutine handles. The only reason to do this is to allow the return object (and the promise that delivers it) to actually cause the resumption of the coroutine. So this is one bit of machinery that exists solely to transform suspend-up into suspend-down; a suspend-up-based promise doesn't care about the handle.

And speaking of such machinery, there is P0057R1's `await_transform` mechanic. This is a mechanism by which the promise can get involved in each `co_await` expression.

Why does the promise object need to involve itself in the particular await style that's happening? This is in part to allow a promise to decide if it should always suspend regardless of whether the value is ready (thus "solving" one of the criticisms of the proposal). But it also exists to allow a single promise to support suspend-up and suspend-down, depending on what is being awaited on. This means that the promise can force suspend down or up, that the promise can involve itself in the decision of how to schedule the resumption.

This leads to the behavior of `co_await` being a highly complicated convergence of the type of the expression, the overload for operator co_await on that type, and the promise type (which itself is derived in part from the function's return type and possibly its parameters).

It seems like every time a problem is raised with the proposal, the solution is to make it more complex. `operator await` was added to allow users to await on types that don't have the await machinery already in them. `await_transform` was added so that promises could get involved in the await process. And so forth.

When you patch something so much that the result looks like a mass of patches rather than a logical feature, you have to ask yourself a question: is this the right way to solve the problem to begin with?

I'm not saying that resumable expressions is better. It's a fine tool for suspend-down, but it absolutely sucks at continuation support. Indeed, most suspend-down proposal suck at continuations. Even N4398 is really just suspend-down with possible compiler-based optimizations; it's just as bad at continuations as any other suspend-down model.

What I think we need is a low-level continuation syntax that is based on just the core feature of pausing a function and transferring its resumption to someone else (the latter part being what resumable expressions, P0099, and all other suspend-down models are terrible at).

The main problem with it is that, to make continuations work, you need to be able to do all of the following. But they all have to happen simultaneously, in a single C++ operation, all locally to that function:

1) return a value to the immediate caller
2) take the object representing the function and give it to someone else, who will schedule its resumption (which may or may not happen immediately)
3) suspend the function

All the other stuff about promises, awaitables, and such is just syntactic cruft that seems to have accrued around the basic concept. I would suggest that a reconceptualization of P0057, based on just the absolute bare minimum needed to do the above, should be looked at.

The ultimate focus of such a project should be on making a continuation-based model that the networking TS can work effectively with. Because if a continuation-based coroutine model doesn't match well with an API who's asynchronous model is built on continuations, then what good is it?

petke

unread,
Feb 13, 2016, 6:59:06 PM2/13/16
to ISO C++ Standard - Future Proposals
I'm a casual observer. I find your arguments convincing and the topic interesting. The topic of "suspend up" vs "suspend down" being fundamentally different and irreconcilable. I'm a bit surprised there hasn't been any replies here (in almost 3 months). Have you gotten some feedback on this through other channels? Or has there been any new developments on this topic?

Regards /Petke
Reply all
Reply to author
Forward
0 new messages