This is an archive of the discontinued LLVM Phabricator instance.

[C++20] [Coroutines] Mark await_suspend as noinline if the awaiter is not empty
ClosedPublic

Authored by ChuanqiXu on Aug 13 2023, 10:40 PM.

Details

Summary

Close https://github.com/llvm/llvm-project/issues/56301
Close https://github.com/llvm/llvm-project/issues/64151

See the summary and the discussion of https://reviews.llvm.org/D157070 to get the full context.

As @rjmccall pointed out, the key point of the root cause is that currently we didn't implement the semantics well ("after the await-ready returns false, the coroutine is considered to be suspended ") well. Since the semantics implies that we (the compiler) shouldn't write the spills into the coroutine frame in the await_suspend. Now it is possible due to some combinations of the optimizations so the semantics are broken. And the inlining is the root optimization of such optimizations. So in this patch, we tried to add the noinline attribute to the await_suspend call.

Also as an optimization, we don't add the noinline attribute to the await_suspend call if the awaiter is an empty class. This should be correct since the programmers can't access the local variables in await_suspend if the awaiter is empty. I think this is necessary for the performance since it is pretty common.

Another potential optimization chance is to remove the noinline attribute in CoroSplit so that we can still inline it finally. But I didn't do that in this patch since I want to separate the llvm patches and clang patches as much as possible.

Diff Detail

Event Timeline

ChuanqiXu created this revision.Aug 13 2023, 10:40 PM
Herald added a project: Restricted Project. · View Herald TranscriptAug 13 2023, 10:40 PM
ChuanqiXu requested review of this revision.Aug 13 2023, 10:40 PM
Herald added a project: Restricted Project. · View Herald TranscriptAug 13 2023, 10:40 PM
Herald added a subscriber: cfe-commits. · View Herald Transcript
ChuanqiXu edited the summary of this revision. (Show Details)Aug 13 2023, 10:41 PM
ChuanqiXu added inline comments.
clang/lib/CodeGen/CGCoroutine.cpp
157–168

Note that the may cause a crash if there are some types we are not able to covered. This is intentional. Since such crash is easy to fix by adding new type to the TypeVisitor. I guess this may be understandable since the type system of clang frontend (or should I say C++?) is really complex.

Also note that this only matters with people who enabled assertions, possibly developers or testers.

For end users who use a clean release build, it is completely OK to not match the type. They may only need to pay 1 bit memory overhead for that. I feel this is better for the user experience.

rjmccall added inline comments.Aug 14 2023, 11:10 AM
clang/lib/CodeGen/CGCall.cpp
5519

Suggestion:

The await_suspend call performed by co_await is essentially asynchronous
to the execution of the coroutine. Inlining it normally into an unsplit coroutine
can cause miscompilation because the coroutine CFG misrepresents the true
control flow of the program: things that happen in the await_suspend are not
guaranteed to happen prior to the resumption of the coroutine, and things that
happen after the resumption of the coroutine (including its exit and the
potential deallocation of the coroutine frame) are not guaranteed to happen
only after the end of await_suspend.

The short-term solution to this problem is to mark the call as uninlinable.
But we don't want to do this if the call is known to be trivial, which is very
common.

We don't need to get into the long-term solution here, but I'd suggest a pattern
like:

call @llvm.coro.await_suspend(token %suspend, ptr %awaiter, ptr @awaitSuspendFn)

and then we just replace that with the normal call during function splitting. I guess we'd have to make sure we did everything right with the return values and exceptions, which might be a pain. But it does have the really nice property that we could move all of this "is it trivial" analysis into the LLVM passes: if we decide that it's safe to inline the function before splitting (e.g. awaitSuspendFn doesn't contain any uses of this), then the optimization is as simple as doing the replacement in CoroEarly instead of after splitting.

clang/lib/CodeGen/CGCoroutine.cpp
145

Please just do getNonReferenceType()->getAsCXXRecordDecl() and conservatively say it might escape if that's null. And you really don't need to repeat the well-formedness checks about the awaiter type that Sema is presumably already going to have done; that's just asking for extra work if this code ever diverges from Sema, e.g. if the language standard changes. So you should be able to do all of this without a visitor.

164

I'm sorry, but I'm really confused about what you're doing here. In general, we really don't want compiler behavior to change based on whether we're running a release version of the compiler. And this would be disabling the optimization completely in release builds?

Are you maybe trying to check whether we're doing an unoptimized build of the *program*? That's CodeGenOpts.OptimizationLevel == 0.

clang/lib/CodeGen/CodeGenFunction.h
351

(1) I'd prefer that we use the term "escape" over "leak" here.
(2) None of these bugs require us to escape the coroutine handle.

Maybe mustPreventInliningOfAwaitSuspend? It's not really a general property.

ChuanqiXu updated this revision to Diff 550181.Aug 14 2023, 8:24 PM

Address comments:

  • Remove the complicated TypeVisitor, use the simple getNonReferenceType()->getAsCXXRecordDecl() form instead.
  • Reword *leak* to *escape*.
  • Use the suggested comments.
ChuanqiXu marked 4 inline comments as done.Aug 14 2023, 8:29 PM

Address comments. Thanks for reviewing.

clang/lib/CodeGen/CGCall.cpp
5519

Got it. The proposed long term solution looks pretty good indeed.

5527

nit: I thought to simplify this to maySuspendLeakCoroutineHandle(). But I feel the current combination reads better. I read it as "we are in a suspend block and it may leak the coroutine handle". I just feel it has better readability.

clang/lib/CodeGen/CGCoroutine.cpp
164

I'm sorry, but I'm really confused about what you're doing here. In general, we really don't want compiler behavior to change based on whether we're running a release version of the compiler. And this would be disabling the optimization completely in release builds?

I was trying to generate the better code at much as I can. But as your advice shows, it is clearly much better to do the analysis in the middle end. So let's try to improve it in the middle end and leave it as is in the frontend.

Are you maybe trying to check whether we're doing an unoptimized build of the *program*? That's CodeGenOpts.OptimizationLevel == 0.

Given now it is pretty simple, I think we can omit the check.

clang/lib/CodeGen/CodeGenFunction.h
351

Used the term escaped instead of leak.

(2) None of these bugs require us to escape the coroutine handle.

I feel like from the perspective of coroutine itself, it looks like the coroutine handle escapes from the coroutine by await_suspend. So I feel this may not be a bad name. Also as the above comments shows, we'd like to improve this in the middle end finally, so these names would be removed in the end of the day too.

rjmccall added inline comments.Aug 15 2023, 10:58 AM
clang/docs/ReleaseNotes.rst
158

Suggestion:

- Fixed an issue where accesses to the local variables of a coroutine during
  ``await_suspend`` could be misoptimized, including accesses to the awaiter
  object itself.  (`#56301 <https://github.com/llvm/llvm-project/issues/56301>`_)
clang/lib/CodeGen/CGCoroutine.cpp
159
169

Is it possible for the awaiter type to be incomplete here? That shouldn't be possible if the awaiter object is returned as an r-value, but as I understand it, you can also return a reference to an object, which would normally allow the type of that object to be incomplete. But maybe that's disallowed due to higher-level rules with coroutines.

clang/lib/CodeGen/CodeGenFunction.h
351

Okay, that makes sense to me.

ChuanqiXu updated this revision to Diff 550955.Aug 16 2023, 6:49 PM
ChuanqiXu marked 4 inline comments as done.

Address comments.

ChuanqiXu marked 2 inline comments as done.Aug 16 2023, 6:51 PM
ChuanqiXu added inline comments.
clang/lib/CodeGen/CGCoroutine.cpp
169

It is not possible since we need to make sure the awaiter has await_ready(), await_suspend() and await_resume member functions.

rjmccall accepted this revision.Aug 21 2023, 3:43 PM

Alright, LGTM.

This revision is now accepted and ready to land.Aug 21 2023, 3:43 PM