Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lewissbaker opened this issue Aug 16, 2017 · 3 comments
Open

Comments

@lewissbaker
Copy link
Owner

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
Copy link
Owner Author

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
Copy link
Owner Author

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
Copy link
Owner Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant