cppcoro icon indicating copy to clipboard operation
cppcoro copied to clipboard

Allow 'co_return task' as way of implementing tail-recursion of tasks

Open lewissbaker opened this issue 6 years ago • 3 comments

If a coroutine wants to return the value of another task as its result then you currently need to co_await the other task first and then co_return the result of that. eg.

lazy_task<T> foo(int n);

lazy_task<T> bar()
{
  co_return co_await foo(123);
}

task_task<> usage()
{
  lazy_task<T> t = bar();
  T result = co_await t;
}

However, this means that the coroutine frame of bar() remains alive until foo() completes as the bar() coroutine is registered as the continuation of the foo() task.

If the coroutine frame of bar() is large then this means we're not releasing the memory/resources of the bar coroutine frame as soon as we could which can lead to additional memory pressure in applications.

It is also potentially problematic for recursive tasks where the recursion depth could be arbitrarily deep, the memory consumption of the call chain could be unbounded.

It would be nice if instead we could perform a tail-recursion here by using co_return task instead of co_return co_await task. This would effectively have the semantics of moving the returned task into the lazy_task<T> object that is being awaited at the top of the call chain. ie. that the result of the returned task becomes the result of the current coroutine.

This would allow the current coroutine frame to be freed before resuming the returned task. This means that in a purely-tail-recursive set of tasks that we'd have at most 2 coroutine frames in existence at one time, even if the recursion had unbounded depth.

eg.

lazy_task<T> foo();

lazy_task<T> bar()
{
  co_return foo(123); // Don't co_await foo() task before returning.
}

task_task<> usage()
{
  lazy_task<T> t = bar();
  T result = co_await t;
}

The co_return foo(123); statement basically moves the lazy_task<T> value returned by foo() call into the t local variable being awaited in usage(). This frees the bar() coroutine frame before then resuming the foo() coroutine.

lewissbaker avatar Aug 16 '17 01:08 lewissbaker

Note that until we have access to the coroutine_handle<> await_suspend(coroutine_handle<> h) overload that guarantees tail-recursive resumption of another coroutine after suspending the current coroutine, the recursive invocation of nested coroutines may still consume some space on the regular stack, even if the resources used by coroutine frames are cleaned up eagerly.

See @GorNishanov's https://github.com/GorNishanov/clang/tree/tailcall branch for experimental implementation of await_suspend() overload in Clang.

See also https://github.com/lewissbaker/cppcoro/tree/lazy_task_tailcall branch for an example implementation of this co_return task feature for lazy_task

lewissbaker avatar Aug 16 '17 01:08 lewissbaker

We won't be able to implement this for lazy_task<void> yet, however, since the current coroutines TS disallows mixing return_void() and return_value() in the same promise_type.

lewissbaker avatar Aug 16 '17 01:08 lewissbaker

I've written up a draft paper that suggests relaxing the restrictions on co_return here: https://github.com/lewissbaker/papers/blob/master/isocpp/relax-coreturn-restrictions.md

lewissbaker avatar Dec 04 '17 22:12 lewissbaker