[llvm] [Coroutines] Implement elision for noop coroutines (PR #174353)
Weibo He via llvm-commits
llvm-commits at lists.llvm.org
Sun Jan 11 18:05:36 PST 2026
https://github.com/NewSigma updated https://github.com/llvm/llvm-project/pull/174353
>From 9e87f3fdef37c5a70301c7522f1df741563b10ae Mon Sep 17 00:00:00 2001
From: NewSigma <NewSigma at 163.com>
Date: Mon, 5 Jan 2026 11:09:39 +0800
Subject: [PATCH 1/3] [Coroutines] Implement elision for noop coroutines
---
llvm/docs/Coroutines.rst | 4 +-
.../lib/Transforms/Coroutines/CoroCleanup.cpp | 75 +++++++-
llvm/lib/Transforms/Coroutines/CoroEarly.cpp | 172 ++++++------------
llvm/lib/Transforms/Coroutines/CoroElide.cpp | 105 +++++++----
.../AddressSanitizer/skip-coro.ll | 3 +-
.../Transforms/Coroutines/coro-elide-noop.ll | 13 ++
.../Transforms/Coroutines/coro-noop-pacbti.ll | 2 +-
llvm/test/Transforms/Coroutines/coro-noop.ll | 4 +-
8 files changed, 212 insertions(+), 166 deletions(-)
create mode 100644 llvm/test/Transforms/Coroutines/coro-elide-noop.ll
diff --git a/llvm/docs/Coroutines.rst b/llvm/docs/Coroutines.rst
index 0e6b49c84acee..e6759f4edf353 100644
--- a/llvm/docs/Coroutines.rst
+++ b/llvm/docs/Coroutines.rst
@@ -2174,7 +2174,9 @@ allocation elision optimization. If so, it replaces
`coro.begin` intrinsic with an address of a coroutine frame placed on its caller
and replaces `coro.alloc` and `coro.free` intrinsics with `false` and `null`
respectively to remove the deallocation code.
-This pass also replaces `coro.resume` and `coro.destroy` intrinsics with direct
+This pass also eliminates the resume and destroy operation on noop coroutines and
+attempts to erase unused `coro.noop` instructions.
+Finally, this pass replaces `coro.resume` and `coro.destroy` intrinsics with direct
calls to resume and destroy functions for a particular coroutine where possible.
CoroCleanup
diff --git a/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp b/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
index 81efca9dfd209..40aa820bf50c2 100644
--- a/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
@@ -8,6 +8,7 @@
#include "llvm/Transforms/Coroutines/CoroCleanup.h"
#include "CoroInternal.h"
+#include "llvm/IR/DIBuilder.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstIterator.h"
@@ -23,8 +24,13 @@ namespace {
// Created on demand if CoroCleanup pass has work to do.
struct Lowerer : coro::LowererBase {
IRBuilder<> Builder;
+ Constant *NoopCoro = nullptr;
+
Lowerer(Module &M) : LowererBase(M), Builder(Context) {}
bool lower(Function &F);
+
+private:
+ void lowerCoroNoop(IntrinsicInst *II);
};
}
@@ -43,6 +49,25 @@ static void lowerSubFn(IRBuilder<> &Builder, CoroSubFnInst *SubFn) {
SubFn->replaceAllUsesWith(Load);
}
+static void buildDebugInfoForNoopResumeDestroyFunc(Function *NoopFn) {
+ Module &M = *NoopFn->getParent();
+ if (M.debug_compile_units().empty())
+ return;
+
+ DICompileUnit *CU = *M.debug_compile_units_begin();
+ DIBuilder DB(M, /*AllowUnresolved*/ false, CU);
+ std::array<Metadata *, 2> Params{nullptr, nullptr};
+ auto *SubroutineType =
+ DB.createSubroutineType(DB.getOrCreateTypeArray(Params));
+ StringRef Name = NoopFn->getName();
+ auto *SP = DB.createFunction(
+ CU, /*Name=*/Name, /*LinkageName=*/Name, /*File=*/CU->getFile(),
+ /*LineNo=*/0, SubroutineType, /*ScopeLine=*/0, DINode::FlagArtificial,
+ DISubprogram::SPFlagDefinition);
+ NoopFn->setSubprogram(SP);
+ DB.finalize();
+}
+
bool Lowerer::lower(Function &F) {
bool IsPrivateAndUnprocessed = F.isPresplitCoroutine() && F.hasLocalLinkage();
bool Changed = false;
@@ -72,6 +97,9 @@ bool Lowerer::lower(Function &F) {
case Intrinsic::coro_id_async:
II->replaceAllUsesWith(ConstantTokenNone::get(Context));
break;
+ case Intrinsic::coro_noop:
+ lowerCoroNoop(II);
+ break;
case Intrinsic::coro_subfn_addr:
lowerSubFn(Builder, cast<CoroSubFnInst>(II));
break;
@@ -108,13 +136,50 @@ bool Lowerer::lower(Function &F) {
return Changed;
}
+void Lowerer::lowerCoroNoop(IntrinsicInst *II) {
+ if (!NoopCoro) {
+ LLVMContext &C = Builder.getContext();
+ Module &M = *II->getModule();
+
+ // Create a noop.frame struct type.
+ auto *FnTy = FunctionType::get(Type::getVoidTy(C), Builder.getPtrTy(0),
+ /*isVarArg=*/false);
+ auto *FnPtrTy = Builder.getPtrTy(0);
+ StructType *FrameTy =
+ StructType::create({FnPtrTy, FnPtrTy}, "NoopCoro.Frame");
+
+ // Create a Noop function that does nothing.
+ Function *NoopFn = Function::createWithDefaultAttr(
+ FnTy, GlobalValue::LinkageTypes::InternalLinkage,
+ M.getDataLayout().getProgramAddressSpace(), "__NoopCoro_ResumeDestroy",
+ &M);
+ NoopFn->setCallingConv(CallingConv::Fast);
+ buildDebugInfoForNoopResumeDestroyFunc(NoopFn);
+ auto *Entry = BasicBlock::Create(C, "entry", NoopFn);
+ ReturnInst::Create(C, Entry);
+
+ // Create a constant struct for the frame.
+ Constant *Values[] = {NoopFn, NoopFn};
+ Constant *NoopCoroConst = ConstantStruct::get(FrameTy, Values);
+ NoopCoro = new GlobalVariable(
+ M, NoopCoroConst->getType(), /*isConstant=*/true,
+ GlobalVariable::PrivateLinkage, NoopCoroConst, "NoopCoro.Frame.Const");
+ cast<GlobalVariable>(NoopCoro)->setNoSanitizeMetadata();
+ }
+
+ Builder.SetInsertPoint(II);
+ auto *NoopCoroVoidPtr = Builder.CreateBitCast(NoopCoro, Int8Ptr);
+ II->replaceAllUsesWith(NoopCoroVoidPtr);
+}
+
static bool declaresCoroCleanupIntrinsics(const Module &M) {
return coro::declaresIntrinsics(
- M, {Intrinsic::coro_alloc, Intrinsic::coro_begin,
- Intrinsic::coro_subfn_addr, Intrinsic::coro_free, Intrinsic::coro_id,
- Intrinsic::coro_id_retcon, Intrinsic::coro_id_async,
- Intrinsic::coro_id_retcon_once, Intrinsic::coro_async_size_replace,
- Intrinsic::coro_async_resume, Intrinsic::coro_begin_custom_abi});
+ M,
+ {Intrinsic::coro_alloc, Intrinsic::coro_begin, Intrinsic::coro_subfn_addr,
+ Intrinsic::coro_free, Intrinsic::coro_id, Intrinsic::coro_id_retcon,
+ Intrinsic::coro_id_async, Intrinsic::coro_id_retcon_once,
+ Intrinsic::coro_noop, Intrinsic::coro_async_size_replace,
+ Intrinsic::coro_async_resume, Intrinsic::coro_begin_custom_abi});
}
PreservedAnalyses CoroCleanupPass::run(Module &M,
diff --git a/llvm/lib/Transforms/Coroutines/CoroEarly.cpp b/llvm/lib/Transforms/Coroutines/CoroEarly.cpp
index cdb58523d1e0e..39ee53f111d28 100644
--- a/llvm/lib/Transforms/Coroutines/CoroEarly.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroEarly.cpp
@@ -8,7 +8,6 @@
#include "llvm/Transforms/Coroutines/CoroEarly.h"
#include "CoroInternal.h"
-#include "llvm/IR/DIBuilder.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/InstIterator.h"
@@ -24,12 +23,10 @@ namespace {
class Lowerer : public coro::LowererBase {
IRBuilder<> Builder;
PointerType *const AnyResumeFnPtrTy;
- Constant *NoopCoro = nullptr;
void lowerResumeOrDestroy(CallBase &CB, CoroSubFnInst::ResumeKind);
void lowerCoroPromise(CoroPromiseInst *Intrin);
void lowerCoroDone(IntrinsicInst *II);
- void lowerCoroNoop(IntrinsicInst *II);
void hidePromiseAlloca(CoroIdInst *CoroId, CoroBeginInst *CoroBegin);
public:
@@ -99,62 +96,6 @@ void Lowerer::lowerCoroDone(IntrinsicInst *II) {
II->eraseFromParent();
}
-static void buildDebugInfoForNoopResumeDestroyFunc(Function *NoopFn) {
- Module &M = *NoopFn->getParent();
- if (M.debug_compile_units().empty())
- return;
-
- DICompileUnit *CU = *M.debug_compile_units_begin();
- DIBuilder DB(M, /*AllowUnresolved*/ false, CU);
- std::array<Metadata *, 2> Params{nullptr, nullptr};
- auto *SubroutineType =
- DB.createSubroutineType(DB.getOrCreateTypeArray(Params));
- StringRef Name = NoopFn->getName();
- auto *SP = DB.createFunction(
- CU, /*Name=*/Name, /*LinkageName=*/Name, /*File=*/ CU->getFile(),
- /*LineNo=*/0, SubroutineType, /*ScopeLine=*/0, DINode::FlagArtificial,
- DISubprogram::SPFlagDefinition);
- NoopFn->setSubprogram(SP);
- DB.finalize();
-}
-
-void Lowerer::lowerCoroNoop(IntrinsicInst *II) {
- if (!NoopCoro) {
- LLVMContext &C = Builder.getContext();
- Module &M = *II->getModule();
-
- // Create a noop.frame struct type.
- auto *FnTy = FunctionType::get(Type::getVoidTy(C), Builder.getPtrTy(0),
- /*isVarArg=*/false);
- auto *FnPtrTy = Builder.getPtrTy(0);
- StructType *FrameTy =
- StructType::create({FnPtrTy, FnPtrTy}, "NoopCoro.Frame");
-
- // Create a Noop function that does nothing.
- Function *NoopFn = Function::createWithDefaultAttr(
- FnTy, GlobalValue::LinkageTypes::InternalLinkage,
- M.getDataLayout().getProgramAddressSpace(), "__NoopCoro_ResumeDestroy",
- &M);
- NoopFn->setCallingConv(CallingConv::Fast);
- buildDebugInfoForNoopResumeDestroyFunc(NoopFn);
- auto *Entry = BasicBlock::Create(C, "entry", NoopFn);
- ReturnInst::Create(C, Entry);
-
- // Create a constant struct for the frame.
- Constant* Values[] = {NoopFn, NoopFn};
- Constant* NoopCoroConst = ConstantStruct::get(FrameTy, Values);
- NoopCoro = new GlobalVariable(M, NoopCoroConst->getType(), /*isConstant=*/true,
- GlobalVariable::PrivateLinkage, NoopCoroConst,
- "NoopCoro.Frame.Const");
- cast<GlobalVariable>(NoopCoro)->setNoSanitizeMetadata();
- }
-
- Builder.SetInsertPoint(II);
- auto *NoopCoroVoidPtr = Builder.CreateBitCast(NoopCoro, Int8Ptr);
- II->replaceAllUsesWith(NoopCoroVoidPtr);
- II->eraseFromParent();
-}
-
// Later middle-end passes will assume promise alloca dead after coroutine
// suspend, leading to misoptimizations. We hide promise alloca using
// coro.promise and will lower it back to alloca at CoroSplit.
@@ -204,64 +145,61 @@ void Lowerer::lowerEarlyIntrinsics(Function &F) {
continue;
switch (CB->getIntrinsicID()) {
- default:
- continue;
- case Intrinsic::coro_begin:
- case Intrinsic::coro_begin_custom_abi:
- if (CoroBegin)
- report_fatal_error(
- "coroutine should have exactly one defining @llvm.coro.begin");
- CoroBegin = cast<CoroBeginInst>(&I);
- break;
- case Intrinsic::coro_free:
- CoroFrees.push_back(cast<CoroFreeInst>(&I));
- break;
- case Intrinsic::coro_suspend:
- // Make sure that final suspend point is not duplicated as CoroSplit
- // pass expects that there is at most one final suspend point.
- if (cast<CoroSuspendInst>(&I)->isFinal())
- CB->setCannotDuplicate();
- HasCoroSuspend = true;
- break;
- case Intrinsic::coro_end_async:
- case Intrinsic::coro_end:
- // Make sure that fallthrough coro.end is not duplicated as CoroSplit
- // pass expects that there is at most one fallthrough coro.end.
- if (cast<AnyCoroEndInst>(&I)->isFallthrough())
- CB->setCannotDuplicate();
- break;
- case Intrinsic::coro_noop:
- lowerCoroNoop(cast<IntrinsicInst>(&I));
- break;
- case Intrinsic::coro_id:
- if (auto *CII = cast<CoroIdInst>(&I)) {
- if (CII->getInfo().isPreSplit()) {
- assert(F.isPresplitCoroutine() &&
- "The frontend uses Switch-Resumed ABI should emit "
- "\"presplitcoroutine\" attribute for the coroutine.");
- setCannotDuplicate(CII);
- CII->setCoroutineSelf();
- CoroId = cast<CoroIdInst>(&I);
- }
+ default:
+ continue;
+ case Intrinsic::coro_begin:
+ case Intrinsic::coro_begin_custom_abi:
+ if (CoroBegin)
+ report_fatal_error(
+ "coroutine should have exactly one defining @llvm.coro.begin");
+ CoroBegin = cast<CoroBeginInst>(&I);
+ break;
+ case Intrinsic::coro_free:
+ CoroFrees.push_back(cast<CoroFreeInst>(&I));
+ break;
+ case Intrinsic::coro_suspend:
+ // Make sure that final suspend point is not duplicated as CoroSplit
+ // pass expects that there is at most one final suspend point.
+ if (cast<CoroSuspendInst>(&I)->isFinal())
+ CB->setCannotDuplicate();
+ HasCoroSuspend = true;
+ break;
+ case Intrinsic::coro_end_async:
+ case Intrinsic::coro_end:
+ // Make sure that fallthrough coro.end is not duplicated as CoroSplit
+ // pass expects that there is at most one fallthrough coro.end.
+ if (cast<AnyCoroEndInst>(&I)->isFallthrough())
+ CB->setCannotDuplicate();
+ break;
+ case Intrinsic::coro_id:
+ if (auto *CII = cast<CoroIdInst>(&I)) {
+ if (CII->getInfo().isPreSplit()) {
+ assert(F.isPresplitCoroutine() &&
+ "The frontend uses Switch-Resumed ABI should emit "
+ "\"presplitcoroutine\" attribute for the coroutine.");
+ setCannotDuplicate(CII);
+ CII->setCoroutineSelf();
+ CoroId = cast<CoroIdInst>(&I);
}
- break;
- case Intrinsic::coro_id_retcon:
- case Intrinsic::coro_id_retcon_once:
- case Intrinsic::coro_id_async:
- F.setPresplitCoroutine();
- break;
- case Intrinsic::coro_resume:
- lowerResumeOrDestroy(*CB, CoroSubFnInst::ResumeIndex);
- break;
- case Intrinsic::coro_destroy:
- lowerResumeOrDestroy(*CB, CoroSubFnInst::DestroyIndex);
- break;
- case Intrinsic::coro_promise:
- lowerCoroPromise(cast<CoroPromiseInst>(&I));
- break;
- case Intrinsic::coro_done:
- lowerCoroDone(cast<IntrinsicInst>(&I));
- break;
+ }
+ break;
+ case Intrinsic::coro_id_retcon:
+ case Intrinsic::coro_id_retcon_once:
+ case Intrinsic::coro_id_async:
+ F.setPresplitCoroutine();
+ break;
+ case Intrinsic::coro_resume:
+ lowerResumeOrDestroy(*CB, CoroSubFnInst::ResumeIndex);
+ break;
+ case Intrinsic::coro_destroy:
+ lowerResumeOrDestroy(*CB, CoroSubFnInst::DestroyIndex);
+ break;
+ case Intrinsic::coro_promise:
+ lowerCoroPromise(cast<CoroPromiseInst>(&I));
+ break;
+ case Intrinsic::coro_done:
+ lowerCoroDone(cast<IntrinsicInst>(&I));
+ break;
}
}
@@ -290,7 +228,7 @@ static bool declaresCoroEarlyIntrinsics(const Module &M) {
M, {Intrinsic::coro_id, Intrinsic::coro_id_retcon,
Intrinsic::coro_id_retcon_once, Intrinsic::coro_id_async,
Intrinsic::coro_destroy, Intrinsic::coro_done, Intrinsic::coro_end,
- Intrinsic::coro_end_async, Intrinsic::coro_noop, Intrinsic::coro_free,
+ Intrinsic::coro_end_async, Intrinsic::coro_free,
Intrinsic::coro_promise, Intrinsic::coro_resume});
}
diff --git a/llvm/lib/Transforms/Coroutines/CoroElide.cpp b/llvm/lib/Transforms/Coroutines/CoroElide.cpp
index 1c8d4a8592d60..169ae360e862f 100644
--- a/llvm/lib/Transforms/Coroutines/CoroElide.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroElide.cpp
@@ -35,10 +35,9 @@ namespace {
// Created on demand if the coro-elide pass has work to do.
class FunctionElideInfo {
public:
- FunctionElideInfo(Function *F) : ContainingFunction(F) {
- this->collectPostSplitCoroIds();
- }
+ FunctionElideInfo(Function *F);
+ bool elideNoopCoro();
bool hasCoroIds() const { return !CoroIds.empty(); }
const SmallVectorImpl<CoroIdInst *> &getCoroIds() const { return CoroIds; }
@@ -46,10 +45,10 @@ class FunctionElideInfo {
private:
Function *ContainingFunction;
SmallVector<CoroIdInst *, 4> CoroIds;
+ SmallVector<IntrinsicInst *, 1> CoroNoops;
// Used in canCoroBeginEscape to distinguish coro.suspend switchs.
SmallPtrSet<const SwitchInst *, 4> CoroSuspendSwitches;
- void collectPostSplitCoroIds();
friend class CoroIdElider;
};
@@ -77,6 +76,35 @@ class CoroIdElider {
};
} // end anonymous namespace
+FunctionElideInfo::FunctionElideInfo(Function *F) : ContainingFunction(F) {
+ for (auto &I : instructions(F)) {
+ auto *II = dyn_cast<IntrinsicInst>(&I);
+ if (!II)
+ continue;
+
+ if (II->getIntrinsicID() == Intrinsic::coro_noop)
+ CoroNoops.push_back(II);
+
+ if (auto *CII = dyn_cast<CoroIdInst>(&I))
+ if (CII->getInfo().isPostSplit())
+ // If it is the coroutine itself, don't touch it.
+ if (CII->getCoroutine() != CII->getFunction())
+ CoroIds.push_back(CII);
+
+ // Consider case like:
+ // %0 = call i8 @llvm.coro.suspend(...)
+ // switch i8 %0, label %suspend [i8 0, label %resume
+ // i8 1, label %cleanup]
+ // and collect the SwitchInsts which are used by escape analysis later.
+ if (auto *CSI = dyn_cast<CoroSuspendInst>(&I))
+ if (CSI->hasOneUse() && isa<SwitchInst>(CSI->use_begin()->getUser())) {
+ SwitchInst *SWI = cast<SwitchInst>(CSI->use_begin()->getUser());
+ if (SWI->getNumCases() == 2)
+ CoroSuspendSwitches.insert(SWI);
+ }
+ }
+}
+
// Go through the list of coro.subfn.addr intrinsics and replace them with the
// provided constant.
static void replaceWithConstant(Constant *Value,
@@ -85,6 +113,29 @@ static void replaceWithConstant(Constant *Value,
replaceAndRecursivelySimplify(I, Value);
}
+bool FunctionElideInfo::elideNoopCoro() {
+ if (CoroNoops.empty())
+ return false;
+
+ bool Changed = false;
+ for (auto *Noop : CoroNoops) {
+ for (User *U : make_early_inc_range(Noop->users())) {
+ if (auto *II = dyn_cast<CoroSubFnInst>(U)) {
+ auto *Call = cast<CallInst>(II->getUniqueUndroppableUser());
+ Call->eraseFromParent();
+ II->eraseFromParent();
+ Changed = true;
+ }
+ }
+
+ if (Noop->user_empty()) {
+ Noop->eraseFromParent();
+ Changed = true;
+ }
+ }
+ return Changed;
+}
+
// See if any operand of the call instruction references the coroutine frame.
static bool operandReferences(CallInst *CI, AllocaInst *Frame, AAResults &AA) {
for (Value *Op : CI->operand_values())
@@ -143,28 +194,6 @@ static std::unique_ptr<raw_fd_ostream> getOrCreateLogFile() {
}
#endif
-void FunctionElideInfo::collectPostSplitCoroIds() {
- for (auto &I : instructions(this->ContainingFunction)) {
- if (auto *CII = dyn_cast<CoroIdInst>(&I))
- if (CII->getInfo().isPostSplit())
- // If it is the coroutine itself, don't touch it.
- if (CII->getCoroutine() != CII->getFunction())
- CoroIds.push_back(CII);
-
- // Consider case like:
- // %0 = call i8 @llvm.coro.suspend(...)
- // switch i8 %0, label %suspend [i8 0, label %resume
- // i8 1, label %cleanup]
- // and collect the SwitchInsts which are used by escape analysis later.
- if (auto *CSI = dyn_cast<CoroSuspendInst>(&I))
- if (CSI->hasOneUse() && isa<SwitchInst>(CSI->use_begin()->getUser())) {
- SwitchInst *SWI = cast<SwitchInst>(CSI->use_begin()->getUser());
- if (SWI->getNumCases() == 2)
- CoroSuspendSwitches.insert(SWI);
- }
- }
-}
-
CoroIdElider::CoroIdElider(CoroIdInst *CoroId, FunctionElideInfo &FEI,
AAResults &AA, DominatorTree &DT,
OptimizationRemarkEmitter &ORE)
@@ -450,23 +479,21 @@ bool CoroIdElider::attemptElide() {
PreservedAnalyses CoroElidePass::run(Function &F, FunctionAnalysisManager &AM) {
auto &M = *F.getParent();
- if (!coro::declaresIntrinsics(M, Intrinsic::coro_id))
+ if (!coro::declaresIntrinsics(M, {Intrinsic::coro_id, Intrinsic::coro_noop}))
return PreservedAnalyses::all();
FunctionElideInfo FEI{&F};
+ bool Changed = FEI.elideNoopCoro();
// Elide is not necessary if there's no coro.id within the function.
- if (!FEI.hasCoroIds())
- return PreservedAnalyses::all();
-
- AAResults &AA = AM.getResult<AAManager>(F);
- DominatorTree &DT = AM.getResult<DominatorTreeAnalysis>(F);
- auto &ORE = AM.getResult<OptimizationRemarkEmitterAnalysis>(F);
-
- bool Changed = false;
- for (auto *CII : FEI.getCoroIds()) {
- CoroIdElider CIE(CII, FEI, AA, DT, ORE);
- Changed |= CIE.attemptElide();
+ if (FEI.hasCoroIds()) {
+ AAResults &AA = AM.getResult<AAManager>(F);
+ DominatorTree &DT = AM.getResult<DominatorTreeAnalysis>(F);
+ auto &ORE = AM.getResult<OptimizationRemarkEmitterAnalysis>(F);
+
+ for (auto *CII : FEI.getCoroIds()) {
+ CoroIdElider CIE(CII, FEI, AA, DT, ORE);
+ Changed |= CIE.attemptElide();
+ }
}
-
return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
}
diff --git a/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll b/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll
index 65b27ee2241dd..387cc2a292f97 100644
--- a/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll
+++ b/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll
@@ -1,5 +1,6 @@
; Tests that asan skips pre-split coroutine and NoopCoro.Frame
-; RUN: opt < %s -S -passes=coro-early,asan | FileCheck %s
+; RUN: opt < %s -S -O0 | FileCheck %s
+; RUN: opt < %s -S -O1 | FileCheck %s
; CHECK: %NoopCoro.Frame = type { ptr, ptr }
; CHECK: @NoopCoro.Frame.Const = private constant %NoopCoro.Frame { ptr @__NoopCoro_ResumeDestroy, ptr @__NoopCoro_ResumeDestroy }
diff --git a/llvm/test/Transforms/Coroutines/coro-elide-noop.ll b/llvm/test/Transforms/Coroutines/coro-elide-noop.ll
new file mode 100644
index 0000000000000..2e59cb6330d8c
--- /dev/null
+++ b/llvm/test/Transforms/Coroutines/coro-elide-noop.ll
@@ -0,0 +1,13 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; Tests that noop coroutine can be elided
+; RUN: opt < %s -S -passes='coro-elide' | FileCheck %s
+
+define void @fn() {
+; CHECK-LABEL: define void @fn() {
+; CHECK-NEXT: ret void
+;
+ %noop.frame = tail call noundef ptr @llvm.coro.noop()
+ %resume = tail call ptr @llvm.coro.subfn.addr(ptr %noop.frame, i8 0)
+ tail call fastcc void %resume(ptr %noop.frame)
+ ret void
+}
diff --git a/llvm/test/Transforms/Coroutines/coro-noop-pacbti.ll b/llvm/test/Transforms/Coroutines/coro-noop-pacbti.ll
index 41a01bea48369..910b6b07e1628 100644
--- a/llvm/test/Transforms/Coroutines/coro-noop-pacbti.ll
+++ b/llvm/test/Transforms/Coroutines/coro-noop-pacbti.ll
@@ -1,5 +1,5 @@
-; RUN: opt < %s -S -passes=coro-early | FileCheck %s
+; RUN: opt < %s -S -passes=coro-cleanup | FileCheck %s
; CHECK: define internal fastcc void @__NoopCoro_ResumeDestroy(ptr %0) #1 {
; CHECK-NEXT: entry:
diff --git a/llvm/test/Transforms/Coroutines/coro-noop.ll b/llvm/test/Transforms/Coroutines/coro-noop.ll
index 7156835e5b2d5..de92df6b9f90b 100644
--- a/llvm/test/Transforms/Coroutines/coro-noop.ll
+++ b/llvm/test/Transforms/Coroutines/coro-noop.ll
@@ -1,5 +1,5 @@
-; Tests that CoroEarly pass correctly lowers coro.noop
-; RUN: opt < %s -S -passes=coro-early | FileCheck %s
+; Tests that CoroCleanup pass correctly lowers coro.noop
+; RUN: opt < %s -S -passes=coro-cleanup | FileCheck %s
; CHECK: %NoopCoro.Frame = type { ptr, ptr }
; CHECK: @NoopCoro.Frame.Const = private constant %NoopCoro.Frame { ptr @__NoopCoro_ResumeDestroy, ptr @__NoopCoro_ResumeDestroy }
>From 83222b7aa75ea413bb97be71acbd82453018e300 Mon Sep 17 00:00:00 2001
From: NewSigma <NewSigma at 163.com>
Date: Mon, 5 Jan 2026 11:24:24 +0800
Subject: [PATCH 2/3] Revert accident formatting
---
llvm/lib/Transforms/Coroutines/CoroEarly.cpp | 108 +++++++++----------
1 file changed, 54 insertions(+), 54 deletions(-)
diff --git a/llvm/lib/Transforms/Coroutines/CoroEarly.cpp b/llvm/lib/Transforms/Coroutines/CoroEarly.cpp
index 39ee53f111d28..3f0c3b6353733 100644
--- a/llvm/lib/Transforms/Coroutines/CoroEarly.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroEarly.cpp
@@ -145,61 +145,61 @@ void Lowerer::lowerEarlyIntrinsics(Function &F) {
continue;
switch (CB->getIntrinsicID()) {
- default:
- continue;
- case Intrinsic::coro_begin:
- case Intrinsic::coro_begin_custom_abi:
- if (CoroBegin)
- report_fatal_error(
- "coroutine should have exactly one defining @llvm.coro.begin");
- CoroBegin = cast<CoroBeginInst>(&I);
- break;
- case Intrinsic::coro_free:
- CoroFrees.push_back(cast<CoroFreeInst>(&I));
- break;
- case Intrinsic::coro_suspend:
- // Make sure that final suspend point is not duplicated as CoroSplit
- // pass expects that there is at most one final suspend point.
- if (cast<CoroSuspendInst>(&I)->isFinal())
- CB->setCannotDuplicate();
- HasCoroSuspend = true;
- break;
- case Intrinsic::coro_end_async:
- case Intrinsic::coro_end:
- // Make sure that fallthrough coro.end is not duplicated as CoroSplit
- // pass expects that there is at most one fallthrough coro.end.
- if (cast<AnyCoroEndInst>(&I)->isFallthrough())
- CB->setCannotDuplicate();
- break;
- case Intrinsic::coro_id:
- if (auto *CII = cast<CoroIdInst>(&I)) {
- if (CII->getInfo().isPreSplit()) {
- assert(F.isPresplitCoroutine() &&
- "The frontend uses Switch-Resumed ABI should emit "
- "\"presplitcoroutine\" attribute for the coroutine.");
- setCannotDuplicate(CII);
- CII->setCoroutineSelf();
- CoroId = cast<CoroIdInst>(&I);
+ default:
+ continue;
+ case Intrinsic::coro_begin:
+ case Intrinsic::coro_begin_custom_abi:
+ if (CoroBegin)
+ report_fatal_error(
+ "coroutine should have exactly one defining @llvm.coro.begin");
+ CoroBegin = cast<CoroBeginInst>(&I);
+ break;
+ case Intrinsic::coro_free:
+ CoroFrees.push_back(cast<CoroFreeInst>(&I));
+ break;
+ case Intrinsic::coro_suspend:
+ // Make sure that final suspend point is not duplicated as CoroSplit
+ // pass expects that there is at most one final suspend point.
+ if (cast<CoroSuspendInst>(&I)->isFinal())
+ CB->setCannotDuplicate();
+ HasCoroSuspend = true;
+ break;
+ case Intrinsic::coro_end_async:
+ case Intrinsic::coro_end:
+ // Make sure that fallthrough coro.end is not duplicated as CoroSplit
+ // pass expects that there is at most one fallthrough coro.end.
+ if (cast<AnyCoroEndInst>(&I)->isFallthrough())
+ CB->setCannotDuplicate();
+ break;
+ case Intrinsic::coro_id:
+ if (auto *CII = cast<CoroIdInst>(&I)) {
+ if (CII->getInfo().isPreSplit()) {
+ assert(F.isPresplitCoroutine() &&
+ "The frontend uses Switch-Resumed ABI should emit "
+ "\"presplitcoroutine\" attribute for the coroutine.");
+ setCannotDuplicate(CII);
+ CII->setCoroutineSelf();
+ CoroId = cast<CoroIdInst>(&I);
+ }
}
- }
- break;
- case Intrinsic::coro_id_retcon:
- case Intrinsic::coro_id_retcon_once:
- case Intrinsic::coro_id_async:
- F.setPresplitCoroutine();
- break;
- case Intrinsic::coro_resume:
- lowerResumeOrDestroy(*CB, CoroSubFnInst::ResumeIndex);
- break;
- case Intrinsic::coro_destroy:
- lowerResumeOrDestroy(*CB, CoroSubFnInst::DestroyIndex);
- break;
- case Intrinsic::coro_promise:
- lowerCoroPromise(cast<CoroPromiseInst>(&I));
- break;
- case Intrinsic::coro_done:
- lowerCoroDone(cast<IntrinsicInst>(&I));
- break;
+ break;
+ case Intrinsic::coro_id_retcon:
+ case Intrinsic::coro_id_retcon_once:
+ case Intrinsic::coro_id_async:
+ F.setPresplitCoroutine();
+ break;
+ case Intrinsic::coro_resume:
+ lowerResumeOrDestroy(*CB, CoroSubFnInst::ResumeIndex);
+ break;
+ case Intrinsic::coro_destroy:
+ lowerResumeOrDestroy(*CB, CoroSubFnInst::DestroyIndex);
+ break;
+ case Intrinsic::coro_promise:
+ lowerCoroPromise(cast<CoroPromiseInst>(&I));
+ break;
+ case Intrinsic::coro_done:
+ lowerCoroDone(cast<IntrinsicInst>(&I));
+ break;
}
}
>From aa8563f24a5ca3e01a95f6432f412cdd4d7cc637 Mon Sep 17 00:00:00 2001
From: NewSigma <NewSigma at 163.com>
Date: Sun, 11 Jan 2026 19:23:53 +0800
Subject: [PATCH 3/3] Move noop coro erasing from CoroElide to CoroCleanup
---
llvm/docs/Coroutines.rst | 4 +-
.../lib/Transforms/Coroutines/CoroCleanup.cpp | 34 +++++-
llvm/lib/Transforms/Coroutines/CoroElide.cpp | 105 +++++++-----------
.../AddressSanitizer/skip-coro.ll | 3 +-
.../Coroutines/coro-cleanup-noop-erase.ll | 24 ++++
.../Transforms/Coroutines/coro-elide-noop.ll | 13 ---
6 files changed, 96 insertions(+), 87 deletions(-)
create mode 100644 llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll
delete mode 100644 llvm/test/Transforms/Coroutines/coro-elide-noop.ll
diff --git a/llvm/docs/Coroutines.rst b/llvm/docs/Coroutines.rst
index e6759f4edf353..0e6b49c84acee 100644
--- a/llvm/docs/Coroutines.rst
+++ b/llvm/docs/Coroutines.rst
@@ -2174,9 +2174,7 @@ allocation elision optimization. If so, it replaces
`coro.begin` intrinsic with an address of a coroutine frame placed on its caller
and replaces `coro.alloc` and `coro.free` intrinsics with `false` and `null`
respectively to remove the deallocation code.
-This pass also eliminates the resume and destroy operation on noop coroutines and
-attempts to erase unused `coro.noop` instructions.
-Finally, this pass replaces `coro.resume` and `coro.destroy` intrinsics with direct
+This pass also replaces `coro.resume` and `coro.destroy` intrinsics with direct
calls to resume and destroy functions for a particular coroutine where possible.
CoroCleanup
diff --git a/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp b/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
index 40aa820bf50c2..6b68cf5bc2c20 100644
--- a/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
@@ -30,6 +30,7 @@ struct Lowerer : coro::LowererBase {
bool lower(Function &F);
private:
+ void elideCoroNoop(IntrinsicInst *II);
void lowerCoroNoop(IntrinsicInst *II);
};
}
@@ -72,7 +73,8 @@ bool Lowerer::lower(Function &F) {
bool IsPrivateAndUnprocessed = F.isPresplitCoroutine() && F.hasLocalLinkage();
bool Changed = false;
- for (Instruction &I : llvm::make_early_inc_range(instructions(F))) {
+ SmallPtrSet<Instruction *, 8> DeadInsts{};
+ for (Instruction &I : instructions(F)) {
if (auto *II = dyn_cast<IntrinsicInst>(&I)) {
switch (II->getIntrinsicID()) {
default:
@@ -98,7 +100,9 @@ bool Lowerer::lower(Function &F) {
II->replaceAllUsesWith(ConstantTokenNone::get(Context));
break;
case Intrinsic::coro_noop:
- lowerCoroNoop(II);
+ elideCoroNoop(II);
+ if (!II->user_empty())
+ lowerCoroNoop(II);
break;
case Intrinsic::coro_subfn_addr:
lowerSubFn(Builder, cast<CoroSubFnInst>(II));
@@ -128,14 +132,38 @@ bool Lowerer::lower(Function &F) {
Target->replaceAllUsesWith(NewFuncPtrStruct);
break;
}
- II->eraseFromParent();
+ DeadInsts.insert(II);
Changed = true;
}
}
+ for (auto *I : DeadInsts)
+ I->eraseFromParent();
return Changed;
}
+void Lowerer::elideCoroNoop(IntrinsicInst *II) {
+ for (User *U : make_early_inc_range(II->users())) {
+ auto *Fn = dyn_cast<CoroSubFnInst>(U);
+ if (Fn == nullptr)
+ continue;
+
+ auto *User = Fn->getUniqueUndroppableUser();
+ if (auto *Call = dyn_cast<CallInst>(User)) {
+ Call->eraseFromParent();
+ Fn->eraseFromParent();
+ continue;
+ }
+
+ if (auto *I = dyn_cast<InvokeInst>(User)) {
+ Builder.SetInsertPoint(I);
+ Builder.CreateBr(I->getNormalDest());
+ I->eraseFromParent();
+ Fn->eraseFromParent();
+ }
+ }
+}
+
void Lowerer::lowerCoroNoop(IntrinsicInst *II) {
if (!NoopCoro) {
LLVMContext &C = Builder.getContext();
diff --git a/llvm/lib/Transforms/Coroutines/CoroElide.cpp b/llvm/lib/Transforms/Coroutines/CoroElide.cpp
index 169ae360e862f..1c8d4a8592d60 100644
--- a/llvm/lib/Transforms/Coroutines/CoroElide.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroElide.cpp
@@ -35,9 +35,10 @@ namespace {
// Created on demand if the coro-elide pass has work to do.
class FunctionElideInfo {
public:
- FunctionElideInfo(Function *F);
+ FunctionElideInfo(Function *F) : ContainingFunction(F) {
+ this->collectPostSplitCoroIds();
+ }
- bool elideNoopCoro();
bool hasCoroIds() const { return !CoroIds.empty(); }
const SmallVectorImpl<CoroIdInst *> &getCoroIds() const { return CoroIds; }
@@ -45,10 +46,10 @@ class FunctionElideInfo {
private:
Function *ContainingFunction;
SmallVector<CoroIdInst *, 4> CoroIds;
- SmallVector<IntrinsicInst *, 1> CoroNoops;
// Used in canCoroBeginEscape to distinguish coro.suspend switchs.
SmallPtrSet<const SwitchInst *, 4> CoroSuspendSwitches;
+ void collectPostSplitCoroIds();
friend class CoroIdElider;
};
@@ -76,35 +77,6 @@ class CoroIdElider {
};
} // end anonymous namespace
-FunctionElideInfo::FunctionElideInfo(Function *F) : ContainingFunction(F) {
- for (auto &I : instructions(F)) {
- auto *II = dyn_cast<IntrinsicInst>(&I);
- if (!II)
- continue;
-
- if (II->getIntrinsicID() == Intrinsic::coro_noop)
- CoroNoops.push_back(II);
-
- if (auto *CII = dyn_cast<CoroIdInst>(&I))
- if (CII->getInfo().isPostSplit())
- // If it is the coroutine itself, don't touch it.
- if (CII->getCoroutine() != CII->getFunction())
- CoroIds.push_back(CII);
-
- // Consider case like:
- // %0 = call i8 @llvm.coro.suspend(...)
- // switch i8 %0, label %suspend [i8 0, label %resume
- // i8 1, label %cleanup]
- // and collect the SwitchInsts which are used by escape analysis later.
- if (auto *CSI = dyn_cast<CoroSuspendInst>(&I))
- if (CSI->hasOneUse() && isa<SwitchInst>(CSI->use_begin()->getUser())) {
- SwitchInst *SWI = cast<SwitchInst>(CSI->use_begin()->getUser());
- if (SWI->getNumCases() == 2)
- CoroSuspendSwitches.insert(SWI);
- }
- }
-}
-
// Go through the list of coro.subfn.addr intrinsics and replace them with the
// provided constant.
static void replaceWithConstant(Constant *Value,
@@ -113,29 +85,6 @@ static void replaceWithConstant(Constant *Value,
replaceAndRecursivelySimplify(I, Value);
}
-bool FunctionElideInfo::elideNoopCoro() {
- if (CoroNoops.empty())
- return false;
-
- bool Changed = false;
- for (auto *Noop : CoroNoops) {
- for (User *U : make_early_inc_range(Noop->users())) {
- if (auto *II = dyn_cast<CoroSubFnInst>(U)) {
- auto *Call = cast<CallInst>(II->getUniqueUndroppableUser());
- Call->eraseFromParent();
- II->eraseFromParent();
- Changed = true;
- }
- }
-
- if (Noop->user_empty()) {
- Noop->eraseFromParent();
- Changed = true;
- }
- }
- return Changed;
-}
-
// See if any operand of the call instruction references the coroutine frame.
static bool operandReferences(CallInst *CI, AllocaInst *Frame, AAResults &AA) {
for (Value *Op : CI->operand_values())
@@ -194,6 +143,28 @@ static std::unique_ptr<raw_fd_ostream> getOrCreateLogFile() {
}
#endif
+void FunctionElideInfo::collectPostSplitCoroIds() {
+ for (auto &I : instructions(this->ContainingFunction)) {
+ if (auto *CII = dyn_cast<CoroIdInst>(&I))
+ if (CII->getInfo().isPostSplit())
+ // If it is the coroutine itself, don't touch it.
+ if (CII->getCoroutine() != CII->getFunction())
+ CoroIds.push_back(CII);
+
+ // Consider case like:
+ // %0 = call i8 @llvm.coro.suspend(...)
+ // switch i8 %0, label %suspend [i8 0, label %resume
+ // i8 1, label %cleanup]
+ // and collect the SwitchInsts which are used by escape analysis later.
+ if (auto *CSI = dyn_cast<CoroSuspendInst>(&I))
+ if (CSI->hasOneUse() && isa<SwitchInst>(CSI->use_begin()->getUser())) {
+ SwitchInst *SWI = cast<SwitchInst>(CSI->use_begin()->getUser());
+ if (SWI->getNumCases() == 2)
+ CoroSuspendSwitches.insert(SWI);
+ }
+ }
+}
+
CoroIdElider::CoroIdElider(CoroIdInst *CoroId, FunctionElideInfo &FEI,
AAResults &AA, DominatorTree &DT,
OptimizationRemarkEmitter &ORE)
@@ -479,21 +450,23 @@ bool CoroIdElider::attemptElide() {
PreservedAnalyses CoroElidePass::run(Function &F, FunctionAnalysisManager &AM) {
auto &M = *F.getParent();
- if (!coro::declaresIntrinsics(M, {Intrinsic::coro_id, Intrinsic::coro_noop}))
+ if (!coro::declaresIntrinsics(M, Intrinsic::coro_id))
return PreservedAnalyses::all();
FunctionElideInfo FEI{&F};
- bool Changed = FEI.elideNoopCoro();
// Elide is not necessary if there's no coro.id within the function.
- if (FEI.hasCoroIds()) {
- AAResults &AA = AM.getResult<AAManager>(F);
- DominatorTree &DT = AM.getResult<DominatorTreeAnalysis>(F);
- auto &ORE = AM.getResult<OptimizationRemarkEmitterAnalysis>(F);
-
- for (auto *CII : FEI.getCoroIds()) {
- CoroIdElider CIE(CII, FEI, AA, DT, ORE);
- Changed |= CIE.attemptElide();
- }
+ if (!FEI.hasCoroIds())
+ return PreservedAnalyses::all();
+
+ AAResults &AA = AM.getResult<AAManager>(F);
+ DominatorTree &DT = AM.getResult<DominatorTreeAnalysis>(F);
+ auto &ORE = AM.getResult<OptimizationRemarkEmitterAnalysis>(F);
+
+ bool Changed = false;
+ for (auto *CII : FEI.getCoroIds()) {
+ CoroIdElider CIE(CII, FEI, AA, DT, ORE);
+ Changed |= CIE.attemptElide();
}
+
return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
}
diff --git a/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll b/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll
index 387cc2a292f97..3a385eac82042 100644
--- a/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll
+++ b/llvm/test/Instrumentation/AddressSanitizer/skip-coro.ll
@@ -1,6 +1,5 @@
; Tests that asan skips pre-split coroutine and NoopCoro.Frame
-; RUN: opt < %s -S -O0 | FileCheck %s
-; RUN: opt < %s -S -O1 | FileCheck %s
+; RUN: opt < %s -S -passes=coro-cleanup,asan | FileCheck %s
; CHECK: %NoopCoro.Frame = type { ptr, ptr }
; CHECK: @NoopCoro.Frame.Const = private constant %NoopCoro.Frame { ptr @__NoopCoro_ResumeDestroy, ptr @__NoopCoro_ResumeDestroy }
diff --git a/llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll b/llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll
new file mode 100644
index 0000000000000..7fd9dc900ddb2
--- /dev/null
+++ b/llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll
@@ -0,0 +1,24 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; Tests that resume or destroy a no-op coroutine can be erased; Finally, erase coro.noop if it has no users.
+; RUN: opt < %s -S -passes='coro-cleanup' | FileCheck %s
+
+define void @fn() personality i32 0 {
+; CHECK-LABEL: define void @fn() personality i32 0 {
+; CHECK-NEXT: [[DONE:.*:]]
+; CHECK-NEXT: ret void
+;
+ %frame = call noundef ptr @llvm.coro.noop()
+ %resume = call ptr @llvm.coro.subfn.addr(ptr %frame, i8 0)
+ call fastcc void %resume(ptr %frame)
+ %destroy = call ptr @llvm.coro.subfn.addr(ptr %frame, i8 1)
+ invoke fastcc void %destroy(ptr %frame)
+ to label %done unwind label %unwind
+
+done:
+ ret void
+
+unwind:
+ %pad = landingpad { ptr, i32 }
+ catch ptr null
+ unreachable
+}
diff --git a/llvm/test/Transforms/Coroutines/coro-elide-noop.ll b/llvm/test/Transforms/Coroutines/coro-elide-noop.ll
deleted file mode 100644
index 2e59cb6330d8c..0000000000000
--- a/llvm/test/Transforms/Coroutines/coro-elide-noop.ll
+++ /dev/null
@@ -1,13 +0,0 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
-; Tests that noop coroutine can be elided
-; RUN: opt < %s -S -passes='coro-elide' | FileCheck %s
-
-define void @fn() {
-; CHECK-LABEL: define void @fn() {
-; CHECK-NEXT: ret void
-;
- %noop.frame = tail call noundef ptr @llvm.coro.noop()
- %resume = tail call ptr @llvm.coro.subfn.addr(ptr %noop.frame, i8 0)
- tail call fastcc void %resume(ptr %noop.frame)
- ret void
-}
More information about the llvm-commits
mailing list