This is an archive of the discontinued LLVM Phabricator instance.

[Coroutines] Mark coroutine as nounwind if all the calls outside the try-catch wouldn't throw.
AbandonedPublic

Authored by ChuanqiXu on Aug 18 2021, 1:26 AM.

Details

Summary

A coroutine would generate following codes:

allocate memories;
if (allocation fails)
  return promise-type::get_return_object_on_allocation_failure();
promise-type promise promise-constructor-arguments ;
auto GRO = promise.get_return_object();
auto init_awaiter = promise.initial_suspend();
init_awaiter.await_ready();
init_awaiter.await_suspend(promise);
try {
    init_awaiter.await_resume();
    function-body
} catch (...) {
    promise.unhandled_exception();
}
co_await promise.final_suspend();

promise_type::final_suspend is guranteed to be noexcept.
So we could mark the coroutine as no-throw if all the followings are
no-throw:

  • Promise Constructor.
  • Parameter Copy/Move Constructor.
  • Memory allocator
  • promise-type::get_return_object_on_allocation_failure().
  • promise.get_return_object().
  • promise.initial_suspend(), init_awaiter.await_ready() and init_awaiter.await_suspend().
  • promise.unhandled_exception()

Test Plan: check-llvm, cppcoro, folly

Diff Detail

Event Timeline

ChuanqiXu requested review of this revision.Aug 18 2021, 1:26 AM
ChuanqiXu created this revision.
ChuanqiXu edited the summary of this revision. (Show Details)Aug 18 2021, 2:00 AM
junparser added inline comments.Aug 18 2021, 3:40 AM
clang/lib/AST/StmtCXX.cpp
146

Without unhandled_exception, we current do not emit try-catch block to catch the exception, so it should return false here.

clang/lib/CodeGen/CGCoroutine.cpp
548

initial_suspend is outside of the try-catch in our implementation now. I'm not sure whether something has been changed here. please double check

we may also conside promise.get_return_object(), even parameter copy/move constructors?

ChuanqiXu updated this revision to Diff 367201.Aug 18 2021, 7:16 AM

Address the comments.

we may also conside promise.get_return_object(), even parameter copy/move constructors?

get_return_object() is covered in the try-catch. I forgot the parameter constructors. Will do.

clang/lib/CodeGen/CGCoroutine.cpp
548

Now the promise:: initial_suspend() and corresponding await_ready and await_suspend is outside of the try-catch. I had fixed it.

ChuanqiXu updated this revision to Diff 367392.Aug 18 2021, 8:03 PM

Handle Param Moves.

ChuanqiXu updated this revision to Diff 367699.Aug 19 2021, 7:59 PM
ChuanqiXu edited the summary of this revision. (Show Details)

Sorry that I found get_return_object and get_return_object_on_allocation_failure are also outside of the try..catch block. Handled them in this revision.

what about allocation function?

BTW,with so many function to check, can we do this in ir level?

ChuanqiXu updated this revision to Diff 368061.Aug 23 2021, 3:51 AM
ChuanqiXu edited the summary of this revision. (Show Details)

Handle allocation function.

what about allocation function?

BTW,with so many function to check, can we do this in ir level?

Sorry for forgetting the allocation function. After handle it, I think the list should be complete.

Maybe it is possible to do this in middle end. But it looks harder than this approach. I would add it to my to-do list.

Thanks for working on this!
A few questions:

  • Is this primarily an optimization, or does it also fix a bug?
  • Does LLVM mid-end not have a nothrow attribute analysis pass? Doing this manually in the front-end seems a bit fragile (concerned about missing something)
  • Do we expect those isXXXNoThrow methods to be called else where? If not, is there a reason we choose to put them in 6 separate functions, instead of one function?

Thanks for working on this!
A few questions:

  • Is this primarily an optimization, or does it also fix a bug?

Yes, it is purely an optimization. On the one hand, this could convert invoke coro; into call coro;. On the other hand, it would reduce the exceptional path, which is helpful for other analysis.

  • Does LLVM mid-end not have a nothrow attribute analysis pass? Doing this manually in the front-end seems a bit fragile (concerned about missing something)

From my perspective, the LLVM mid-end doesn't have a nothrow analysis pass as I expected in this patch. For example:

void bar();
void foo() {
    try {
        bar();
    } catch(...) {

    }
}
void baz() {
    try {
        foo();
    } catch(...) {

    }
}

foo wouldn't be marked as no-throw even if we use O3. BTW, if we mark bar() as noexcept, foo() and bad() would get optimized correctly.

  • Do we expect those isXXXNoThrow methods to be called else where? If not, is there a reason we choose to put them in 6 separate functions, instead of one function?

The reason why I use 6 calls instead of 1 is that I think it is more clear. The 6 calls are easy to understand once the reader reads the comment. And if we use one function, I am afraid that the reader need one more jump to read the definition.

From your description, it sounds like the optimizer doesn't make any effort to compute nounwind bottom-up on the call graph. That seems like something that, if it can be fixed, would be a lot simpler and have a lot more general applicability than what you're doing here.

clang/lib/AST/Expr.cpp
3774 ↗(On Diff #368061)

If this is ultimately supposed to be a "does this expression throw" condition, doing that condition correctly is a lot more complicated than this. Some of the complexity in Sema::canThrow wouldn't be necessary here because it allows for dependent expressions, but a lot of it is.

ChuanqiXu updated this revision to Diff 368297.Aug 24 2021, 1:11 AM
ChuanqiXu retitled this revision from [Coroutines] Mark coroutine as nounwind if ctor of promise_type and unhandled_exception is noexcept to [Coroutines] Mark coroutine as nounwind if all the calls outside the try-catch wouldn't throw..

Address comments:

  • Call one function in CGCoroutine.cpp::EmitCoroutineBody instead of 6 calls.
  • Use Sema::canThrow to replace self-implemented Expr::hasMayThrowCall

From your description, it sounds like the optimizer doesn't make any effort to compute nounwind bottom-up on the call graph. That seems like something that, if it can be fixed, would be a lot simpler and have a lot more general applicability than what you're doing here.

Yes. It is clear that if a pass to calculate the nounwind attribute in the middle end would be much more general than this one, which applies to coroutine only. And I am looking at the documents now : ). And I understand that it is a much higher bar to implement a pass and turn it on by default.
And I think this patch is still meaningful even after we had a pass to calculate nounwind attribute. Since this approach is faster. Thanks for the SemaCoroutine which would calculate the structure of coroutine body statement. We could check the corresponding node of the coroutine body statement directly.

ChuanqiXu updated this revision to Diff 368298.Aug 24 2021, 1:27 AM

Remove unused header.

LGTM, @rjmccall @lxfind any other comments?

junparser accepted this revision.Aug 26 2021, 7:13 PM
This revision is now accepted and ready to land.Aug 26 2021, 7:13 PM
rjmccall requested changes to this revision.Aug 26 2021, 7:33 PM
rjmccall added inline comments.
clang/include/clang/AST/StmtCXX.h
335

Please add a CoroutineStmtBitfields for this and NumParams. You can follow the pattern of e.g. LabelStmtBitfields.

464

You've written "class" instead of "method" here. Also, this isn't really a "helper" anything, it's just a query on the statement.

The name should be clear that you're really just talking about whether the initial "ramp" portion of the coroutine can throw.

Can a coroutine have a function-try-block as their body, and if so, what happens to exceptions from the generated ramp portion of the coroutine?

This revision now requires changes to proceed.Aug 26 2021, 7:33 PM
ChuanqiXu updated this revision to Diff 369035.EditedAug 27 2021, 12:04 AM
  • Refactor CoroutineBodyStmt::MayThrow and CoroutineBodyStmt::NumParams into Stmt::CoroutineBodyStmtBitfields.
  • Rephrase the comment for CoroutineBodyStmt::mayThrow.

This revision also touched ASTWriter and ASTReader. But I didn't find the place to test for ASTStmtWriter and ASTStmtReader.

ChuanqiXu added inline comments.Aug 27 2021, 12:13 AM
clang/include/clang/AST/StmtCXX.h
335

Thanks for suggestion!

464

The name should be clear that you're really just talking about whether the initial "ramp" portion of the coroutine can throw.

This may not be precise. Since the condition should be whether the ramp part and the promise_type::unhandled_exception() can throw. So I think mayThrow tells whether or not the coroutine can throw.

Can a coroutine have a function-try-block as their body, and if so, what happens to exceptions from the generated ramp portion of the coroutine?

If that happens, the exceptions would be thrown to the caller of the coroutine instead of the catch block in the coroutine nor the corresponding promise_type::unhandled_exception().

We also have to worry about throwing destructors for all of the introduced variables, right?

I'm still not sold on the idea that we should be doing this in the frontend. I mean, your argument could really apply to any function, not just coroutines.

Alternatively, if we do want the frontend to compute a simple, local nounwind, we could probably do it in IRGen, not by inspecting the AST but by just remembering at the end of the function if we ever called EmitCallOrInvoke in a context that doesn't catch all exceptions.

clang/include/clang/AST/StmtCXX.h
464

This may not be precise. Since the condition should be whether the ramp part and the promise_type::unhandled_exception() can throw. So I think mayThrow tells whether or not the coroutine can throw.

Oh, I see your point, sorry about that.

clang/lib/CodeGen/CGCoroutine.cpp
572

This whole comment is out of place now, I think.

ChuanqiXu planned changes to this revision.Aug 29 2021, 6:57 PM

We also have to worry about throwing destructors for all of the introduced variables, right?

I think what you mean is ~promise()? Did I understand right? It looks like that we indeed to forget that before.

I'm still not sold on the idea that we should be doing this in the frontend. I mean, your argument could really apply to any function, not just coroutines.

Alternatively, if we do want the frontend to compute a simple, local nounwind, we could probably do it in IRGen, not by inspecting the AST but by just remembering at the end of the function if we ever called EmitCallOrInvoke in a context that doesn't catch all exceptions.

Thanks for suggestion! The alternative makes sense to me. I would like to see actually how hard it is to implement this in middle end. And if it is really so hard, I would like to try your solution. Thanks again.