-
Notifications
You must be signed in to change notification settings - Fork 10.8k
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
[Coroutines] Drop dead instructions more aggressively in addMustTailToCoroResumes() #85271
Conversation
…oCoroResumes() The old code used isInstructionTriviallyDead() when walking the path from a resume call to function return to check if the call is in tail position. However, since the code was walking forwards it was not able to get past instructions such as: %gep = getelementptr inbounds i64, ptr %alloc.var, i32 0 %foo = ptrtoint ptr %gep to i64 This patch calls SimplifyInstructionsInBlock() before walking a block to remove any dead instructions in a more rigorous fashion.
@llvm/pr-subscribers-clang @llvm/pr-subscribers-llvm-transforms Author: Hans (zmodem) ChangesThe old code used isInstructionTriviallyDead() when walking the path from a resume call to function return to check if the call is in tail position. However, since the code was walking forwards it was not able to get past instructions such as: %gep = getelementptr inbounds i64, ptr %alloc.var, i32 0 This patch calls SimplifyInstructionsInBlock() before walking a block to remove any dead instructions in a more rigorous fashion. Full diff: https://github.com/llvm/llvm-project/pull/85271.diff 2 Files Affected:
diff --git a/llvm/lib/Transforms/Coroutines/CoroSplit.cpp b/llvm/lib/Transforms/Coroutines/CoroSplit.cpp
index 0fce596d3e2ca0..5d3106f157edcd 100644
--- a/llvm/lib/Transforms/Coroutines/CoroSplit.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroSplit.cpp
@@ -1203,10 +1203,6 @@ static bool simplifyTerminatorLeadingToRet(Instruction *InitialInst) {
if (isa<BitCastInst>(I) || I->isDebugOrPseudoInst() ||
I->isLifetimeStartOrEnd())
I = I->getNextNode();
- else if (isInstructionTriviallyDead(I))
- // Duing we are in the middle of the transformation, we need to erase
- // the dead instruction manually.
- I = &*I->eraseFromParent();
else
break;
}
@@ -1245,6 +1241,7 @@ static bool simplifyTerminatorLeadingToRet(Instruction *InitialInst) {
}
BasicBlock *Succ = BR->getSuccessor(SuccIndex);
+ SimplifyInstructionsInBlock(Succ);
scanPHIsAndUpdateValueMap(I, Succ, ResolvedValues);
I = GetFirstValidInstruction(Succ->getFirstNonPHIOrDbgOrLifetime());
@@ -1288,6 +1285,7 @@ static bool simplifyTerminatorLeadingToRet(Instruction *InitialInst) {
return false;
BasicBlock *BB = SI->findCaseValue(Cond)->getCaseSuccessor();
+ SimplifyInstructionsInBlock(BB);
scanPHIsAndUpdateValueMap(I, BB, ResolvedValues);
I = GetFirstValidInstruction(BB->getFirstNonPHIOrDbgOrLifetime());
continue;
diff --git a/llvm/test/Transforms/Coroutines/coro-split-musttail7.ll b/llvm/test/Transforms/Coroutines/coro-split-musttail7.ll
index 2257d5aee473ee..241de5cfb25a7d 100644
--- a/llvm/test/Transforms/Coroutines/coro-split-musttail7.ll
+++ b/llvm/test/Transforms/Coroutines/coro-split-musttail7.ll
@@ -1,6 +1,6 @@
; Tests that sinked lifetime markers wouldn't provent optimization
; to convert a resuming call to a musttail call.
-; The difference between this and coro-split-musttail5.ll and coro-split-musttail5.ll
+; The difference between this and coro-split-musttail5.ll and coro-split-musttail6.ll
; is that this contains dead instruction generated during the transformation,
; which makes the optimization harder.
; RUN: opt < %s -passes='cgscc(coro-split),simplifycfg,early-cse' -S | FileCheck %s
@@ -27,6 +27,11 @@ await.suspend:
%save2 = call token @llvm.coro.save(ptr null)
call fastcc void @fakeresume1(ptr align 8 null)
%suspend2 = call i8 @llvm.coro.suspend(token %save2, i1 false)
+
+ ; These (non-trivially) dead instructions are in the way.
+ %gep = getelementptr inbounds i64, ptr %alloc.var, i32 0
+ %foo = ptrtoint ptr %gep to i64
+
switch i8 %suspend2, label %exit [
i8 0, label %await.ready
i8 1, label %exit
|
else if (isInstructionTriviallyDead(I)) | ||
// Duing we are in the middle of the transformation, we need to erase | ||
// the dead instruction manually. | ||
I = &*I->eraseFromParent(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use RecursivelyDeleteTriviallyDeadInstructions()
here instead? simplify seems overkill.
also do you know why we would end up with dead instructions at this point? we should have already simplified the function when we get to corosplit
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use RecursivelyDeleteTriviallyDeadInstructions() here instead? simplify seems overkill.
In my test case the first instruction isn't trivially dead, so I don't think that would work.
do you know why we would end up with dead instructions at this point? we should have already simplified the function when we get to corosplit
I think such instructions can emerge during corosplit. Maybe @ChuanqiXu9 can answer that one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use RecursivelyDeleteTriviallyDeadInstructions() here instead? simplify seems overkill.
In my test case the first instruction isn't trivially dead, so I don't think that would work.
if we call it on the second instruction, it'll delete the first one as well
do you know why we would end up with dead instructions at this point? we should have already simplified the function when we get to corosplit
I think such instructions can emerge during corosplit. Maybe @ChuanqiXu9 can answer that one.
understanding this a bit better would be good to see if we're doing extra unnecessary work, or if those instructions should be cleaned up earlier in the pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, since these instructions become dead during CoroSplit pass. The internals are
%save2 = call token @llvm.coro.save(ptr null)
call fastcc void @fakeresume1(ptr align 8 null)
%suspend2 = call i8 @llvm.coro.suspend(token %save2, i1 false)
; These (non-trivially) dead instructions are in the way.
%gep = getelementptr inbounds i64, ptr %alloc.var, i32 0
%foo = ptrtoint ptr %gep to i64
switch i8 %suspend2, label %exit [
i8 0, label %await.ready
i8 1, label %exit
]
await.ready:
call void @consume(ptr %alloc.var)
call void @llvm.lifetime.end.p0(i64 1, ptr %alloc.var)
br label %exit
exit:
call i1 @llvm.coro.end(ptr null, i1 false, token none)
ret void
}
During the splitting, the value %suspend2
will become a constant in different splitted functions. So it makes sense if the previous passes can't catch such things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
understanding this a bit better would be good to see if we're doing extra unnecessary work, or if those instructions should be cleaned up earlier in the pass
+1 I'd like to understand this better too :)
In the original code I was debugging, the presplit coroutine has a return value:
169: ; preds = %166, %163, %135, %68, %48
%170 = getelementptr inbounds i8, ptr %17, i64 -16
%171 = ptrtoint ptr %170 to i64
%172 = call i1 @llvm.coro.end(ptr null, i1 false) #19
ret i64 %171
}
corosplit clones the function into various versions, and the modifications of those clones are what causes dead code, makes some values constant, etc.
In my case, CoroCloner::create()
will make the original return unreachable:
switch (Shape.ABI) {
// In these ABIs, the cloned functions always return 'void', and the
// existing return sites are meaningless. Note that for unique
// continuations, this includes the returns associated with suspends;
// this is fine because we can't suspend twice.
case coro::ABI::Switch:
case coro::ABI::RetconOnce:
// Remove old returns.
for (ReturnInst *Return : Returns)
changeToUnreachable(Return);
break;
and replaceCoroEnds()
will replace the @llvm.coro.end
by ret void
.
So perhaps CoroCloner is the one who should clean up these instructions.
On the other hand, it seems corosplit relies on later passes to clean up this stuff, which is maybe fine (especially since it also runs at -O0
), except that it's a problem for addMustTailToCoroResumes()
.
simplify seems overkill
Yes, maybe it's too big a hammer. On the other hand, the ad-hoc dce and constant folding in simplifyTerminatorLeadingToRet()
makes me a little nervous.
Instead of deleting those dead instructions, maybe we could just look past them. What the code is really trying to do is follow the control flow to a return instruction. I think it should be able to skip any side-effect free instructions on the way as long as the values aren't needed for a conditional branch or such. I'll push a version which does that. Let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So perhaps CoroCloner is the one who should clean up these instructions.
Generally this is not something we'd like to do for the middle end passes. However, this is (was) a defect in the clang/llvm coroutines implementations. Since the symmetric transfer is semantics about C++ and according to our design for modules, we have to make it in the middle end. So this is the why we have this... But probably we can never get rid of this if we (Clang/LLVM) don't want to be conforming to C++ anymore.
Yes, maybe it's too big a hammer. On the other hand, the ad-hoc dce and constant folding in simplifyTerminatorLeadingToRet() makes me a little nervous.
So my line is, if the problem comes from a particular C++ programs and the corresponding LLVM passes applied, yes we need to solve it. However, if it comes from a manual constructed IR only, then possibly we shouldn't do that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks.
Any thoughts on the new commit? @ChuanqiXu9 @aeubanks |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the previous one version. And if the problem comes from a C++ program (I believe it may be the case), we can add the (reduced) C++ program as a test. If it is not the case, I guess we shouldn't do this OR we need to revisit the motivation or the issue or the use cases of switching IR.
continue; | ||
} | ||
|
||
return false; | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC, we shouldn't do this. This is the pattern we generally do for optimizations. But here, it is not an pure optimizations. We are implementing a C++ semantics. (See my above comments for some backgrounds.) So it is better to break early if we have something we are not able to handle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I took another look on this to make sure that I understood this patch correctly. And it looks like my previous comments were confusing and maybe unrelated. So ignore the above comments if it is confusing.
And the comment about wanting a new test from C++ still works.
Since the dirty properties of the corosplit pass mentioned above, it is hard to tell if this patch is correct or not easily. It depends on how the code will be generated from C++ frontend and other passes. So conservatively, this change doesn't cover the original codes since the trivally dead instructions may have side effects. So the conservative change is to make this patch covers previous behavior.
Otherwise, if we want to proceed aggressively, we may need to understand the original motivation issue and the corresponding C++ codes and invest if this is regression or new code patterns we never thought before. But as I said, it may be annoying since the root cause is dirty.
I can accept either the conservative or the aggressive method to proceed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update. I've added a C++ reproducer.
If an instruction has side effects, it will not be considered trivially dead, so I think my patch covers the original cases, and more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an instruction has side effects, it will not be considered trivially dead, so I think my patch covers the original cases, and more.
It is true abstractly. But for LLVM, at least from the comment, it says:
/// Return true if the result produced by the instruction is not used, and the
/// instruction will return. Certain side-effecting instructions are also
/// considered dead if there are no uses of the instruction.
bool isInstructionTriviallyDead(Instruction *I,
const TargetLibraryInfo *TLI = nullptr);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I'll change it to call wouldInstructionBeTriviallyDead()
instead.
My test case does come from a C++ program. Here is a reasonably small reproducer:
Note the missing (In this case, later passes will make that a tail call anyway, at least on X86, but that wasn't the case in the original program which overflowed the stack instead.) (The trick to the reproducer is that the coroutine has a return value the requires a couple of instructions to put together, which blocks |
Thanks. The explanation looks slightly odd since I believe we have such cases before. I am wondering if this is a regression from f786881 (this may not be important) And could you put the reduced example into the patch in somewhere of |
No, it failed before that commit also.
Added Please take another look. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks.
…roResumes() (llvm#85271) The old code used isInstructionTriviallyDead() and removed instructions when walking the path from a resume call to function return to check if the call is in tail position. However, since the code was walking forwards it was not able to get past instructions such as: %gep = getelementptr inbounds i64, ptr %alloc.var, i32 0 %foo = ptrtoint ptr %gep to i64 This patch instead ignores such instructions as long as their values are not needed. This enables the code to emit tail calls in more situations.
The old code used isInstructionTriviallyDead() when walking the path from a resume call to function return to check if the call is in tail position.
However, since the code was walking forwards it was not able to get past instructions such as:
%gep = getelementptr inbounds i64, ptr %alloc.var, i32 0
%foo = ptrtoint ptr %gep to i64
This patch calls SimplifyInstructionsInBlock() before walking a block to remove any dead instructions in a more rigorous fashion.