//===- CoroAnnotationElide.cpp - Elide attributed safe coroutine calls ----===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // \file // This pass transforms all Call or Invoke instructions that are annotated // "coro_elide_safe" to call the `.noalloc` variant of coroutine instead. // The frame of the callee coroutine is allocated inside the caller. A pointer // to the allocated frame will be passed into the `.noalloc` ramp function. // //===----------------------------------------------------------------------===// #include "llvm/Transforms/Coroutines/CoroAnnotationElide.h" #include "llvm/Analysis/CGSCCPassManager.h" #include "llvm/Analysis/LazyCallGraph.h" #include "llvm/Analysis/OptimizationRemarkEmitter.h" #include "llvm/IR/Analysis.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/Module.h" #include "llvm/IR/PassManager.h" #include "llvm/Transforms/Utils/CallGraphUpdater.h" #include "llvm/Transforms/Utils/Cloning.h" #include using namespace llvm; #define DEBUG_TYPE "coro-annotation-elide" static Instruction *getFirstNonAllocaInTheEntryBlock(Function *F) { for (Instruction &I : F->getEntryBlock()) if (!isa(&I)) return &I; llvm_unreachable("no terminator in the entry block"); } // Create an alloca in the caller, using FrameSize and FrameAlign as the callee // coroutine's activation frame. static Value *allocateFrameInCaller(Function *Caller, uint64_t FrameSize, Align FrameAlign) { LLVMContext &C = Caller->getContext(); BasicBlock::iterator InsertPt = getFirstNonAllocaInTheEntryBlock(Caller)->getIterator(); const DataLayout &DL = Caller->getDataLayout(); auto FrameTy = ArrayType::get(Type::getInt8Ty(C), FrameSize); auto *Frame = new AllocaInst(FrameTy, DL.getAllocaAddrSpace(), "", InsertPt); Frame->setAlignment(FrameAlign); return Frame; } // Given a call or invoke instruction to the elide safe coroutine, this function // does the following: // - Allocate a frame for the callee coroutine in the caller using alloca. // - Replace the old CB with a new Call or Invoke to `NewCallee`, with the // pointer to the frame as an additional argument to NewCallee. static void processCall(CallBase *CB, Function *Caller, Function *NewCallee, uint64_t FrameSize, Align FrameAlign) { // TODO: generate the lifetime intrinsics for the new frame. This will require // introduction of two pesudo lifetime intrinsics in the frontend around the // `co_await` expression and convert them to real lifetime intrinsics here. auto *FramePtr = allocateFrameInCaller(Caller, FrameSize, FrameAlign); auto NewCBInsertPt = CB->getIterator(); llvm::CallBase *NewCB = nullptr; SmallVector NewArgs; NewArgs.append(CB->arg_begin(), CB->arg_end()); NewArgs.push_back(FramePtr); if (auto *CI = dyn_cast(CB)) { auto *NewCI = CallInst::Create(NewCallee->getFunctionType(), NewCallee, NewArgs, "", NewCBInsertPt); NewCI->setTailCallKind(CI->getTailCallKind()); NewCB = NewCI; } else if (auto *II = dyn_cast(CB)) { NewCB = InvokeInst::Create(NewCallee->getFunctionType(), NewCallee, II->getNormalDest(), II->getUnwindDest(), NewArgs, {}, "", NewCBInsertPt); } else { llvm_unreachable("CallBase should either be Call or Invoke!"); } NewCB->setCalledFunction(NewCallee->getFunctionType(), NewCallee); NewCB->setCallingConv(CB->getCallingConv()); NewCB->setAttributes(CB->getAttributes()); NewCB->setDebugLoc(CB->getDebugLoc()); std::copy(CB->bundle_op_info_begin(), CB->bundle_op_info_end(), NewCB->bundle_op_info_begin()); NewCB->removeFnAttr(llvm::Attribute::CoroElideSafe); CB->replaceAllUsesWith(NewCB); InlineFunctionInfo IFI; InlineResult IR = InlineFunction(*NewCB, IFI); if (IR.isSuccess()) { CB->eraseFromParent(); } else { NewCB->replaceAllUsesWith(CB); NewCB->eraseFromParent(); } } PreservedAnalyses CoroAnnotationElidePass::run(LazyCallGraph::SCC &C, CGSCCAnalysisManager &AM, LazyCallGraph &CG, CGSCCUpdateResult &UR) { bool Changed = false; CallGraphUpdater CGUpdater; CGUpdater.initialize(CG, C, AM, UR); auto &FAM = AM.getResult(C, CG).getManager(); for (LazyCallGraph::Node &N : C) { Function *Callee = &N.getFunction(); Function *NewCallee = Callee->getParent()->getFunction( (Callee->getName() + ".noalloc").str()); if (!NewCallee) continue; SmallVector Users; for (auto *U : Callee->users()) { if (auto *CB = dyn_cast(U)) { if (CB->getCalledFunction() == Callee) Users.push_back(CB); } } auto FramePtrArgPosition = NewCallee->arg_size() - 1; auto FrameSize = NewCallee->getParamDereferenceableBytes(FramePtrArgPosition); auto FrameAlign = NewCallee->getParamAlign(FramePtrArgPosition).valueOrOne(); auto &ORE = FAM.getResult(*Callee); for (auto *CB : Users) { auto *Caller = CB->getFunction(); if (!Caller) continue; bool IsCallerPresplitCoroutine = Caller->isPresplitCoroutine(); bool HasAttr = CB->hasFnAttr(llvm::Attribute::CoroElideSafe); if (IsCallerPresplitCoroutine && HasAttr) { auto *CallerN = CG.lookup(*Caller); auto *CallerC = CallerN ? CG.lookupSCC(*CallerN) : nullptr; // If CallerC is nullptr, it means LazyCallGraph hasn't visited Caller // yet. Skip the call graph update. auto ShouldUpdateCallGraph = !!CallerC; processCall(CB, Caller, NewCallee, FrameSize, FrameAlign); ORE.emit([&]() { return OptimizationRemark(DEBUG_TYPE, "CoroAnnotationElide", Caller) << "'" << ore::NV("callee", Callee->getName()) << "' elided in '" << ore::NV("caller", Caller->getName()) << "'"; }); FAM.invalidate(*Caller, PreservedAnalyses::none()); Changed = true; if (ShouldUpdateCallGraph) updateCGAndAnalysisManagerForCGSCCPass(CG, *CallerC, *CallerN, AM, UR, FAM); } else { ORE.emit([&]() { return OptimizationRemarkMissed(DEBUG_TYPE, "CoroAnnotationElide", Caller) << "'" << ore::NV("callee", Callee->getName()) << "' not elided in '" << ore::NV("caller", Caller->getName()) << "' (caller_presplit=" << ore::NV("caller_presplit", IsCallerPresplitCoroutine) << ", elide_safe_attr=" << ore::NV("elide_safe_attr", HasAttr) << ")"; }); } } } return Changed ? PreservedAnalyses::none() : PreservedAnalyses::all(); }