[LLVM] Add flatten function attribute to LLVM IR and implement recursive inlining in AlwaysInliner (#174899)
This adds a new function-level `flatten` LLVM IR attribute and implements support for it in the AlwaysInliner pass, bringing LLVM's behavior in line with GCC. Previously, the `flatten` attribute only existed as a Clang attribute, which was lowered to `alwaysinline` on individual call sites. Per the RFC discussion [1], the consensus was to match GCC semantics: recursively inline the entire call tree into the flattened function, rather than just immediate call sites. This PR: - Adds the `flatten` function attribute to LLVM IR - Implements recursive inlining of all viable callees in AlwaysInliner - Uses inline history tracking to detect and stop at recursive call cycles - Emits optimization remarks when inlining is skipped due to recursion A follow-up patch will update Clang to emit the LLVM `flatten` attribute on functions instead of marking individual call sites with `alwaysinline`. [1] https://discourse.llvm.org/t/rfc-function-level-flatten-depth-attribute-for-depth-limited-inlining
This commit is contained in:
parent
90d3944c4a
commit
f66bd8e81a
@ -815,6 +815,7 @@ enum AttributeKindCodes {
|
||||
ATTR_KIND_NO_CREATE_UNDEF_OR_POISON = 105,
|
||||
ATTR_KIND_DENORMAL_FPENV = 106,
|
||||
ATTR_KIND_NOOUTLINE = 107,
|
||||
ATTR_KIND_FLATTEN = 108,
|
||||
};
|
||||
|
||||
enum ComdatSelectionKindCodes {
|
||||
|
||||
@ -134,6 +134,9 @@ def DisableSanitizerInstrumentation: EnumAttr<"disable_sanitizer_instrumentation
|
||||
/// Provide pointer element type to intrinsic.
|
||||
def ElementType : TypeAttr<"elementtype", IntersectPreserve, [ParamAttr]>;
|
||||
|
||||
/// Flatten function by recursively inlining all calls.
|
||||
def Flatten : EnumAttr<"flatten", IntersectPreserve, [FnAttr]>;
|
||||
|
||||
/// Whether to keep return instructions, or replace with a jump to an external
|
||||
/// symbol.
|
||||
def FnRetThunkExtern : EnumAttr<"fn_ret_thunk_extern", IntersectPreserve, [FnAttr]>;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
#ifndef LLVM_TRANSFORMS_UTILS_CLONING_H
|
||||
#define LLVM_TRANSFORMS_UTILS_CLONING_H
|
||||
|
||||
#include "llvm/ADT/ArrayRef.h"
|
||||
#include "llvm/ADT/SmallVector.h"
|
||||
#include "llvm/ADT/Twine.h"
|
||||
#include "llvm/Analysis/AssumptionCache.h"
|
||||
@ -432,6 +433,14 @@ LLVM_ABI void cloneAndAdaptNoAliasScopes(ArrayRef<MDNode *> NoAliasDeclScopes,
|
||||
LLVM_ABI void cloneAndAdaptNoAliasScopes(ArrayRef<MDNode *> NoAliasDeclScopes,
|
||||
Instruction *IStart, Instruction *IEnd,
|
||||
LLVMContext &Context, StringRef Ext);
|
||||
/// Check if Function F appears in the inline history chain.
|
||||
/// InlineHistory is a vector of (Function, ParentHistoryID) pairs.
|
||||
/// Returns true if F was already inlined in the chain leading to
|
||||
/// InlineHistoryID.
|
||||
LLVM_ABI bool
|
||||
inlineHistoryIncludes(Function *F, int InlineHistoryID,
|
||||
ArrayRef<std::pair<Function *, int>> InlineHistory);
|
||||
|
||||
} // end namespace llvm
|
||||
|
||||
#endif // LLVM_TRANSFORMS_UTILS_CLONING_H
|
||||
|
||||
@ -3229,6 +3229,15 @@ std::optional<InlineResult> llvm::getAttributeBasedInliningDecision(
|
||||
if (!functionsHaveCompatibleAttributes(Caller, Callee, CalleeTTI, GetTLI))
|
||||
return InlineResult::failure("conflicting attributes");
|
||||
|
||||
// Flatten: inline all viable calls from flatten functions regardless of cost.
|
||||
// Checked before optnone so that flatten takes priority.
|
||||
if (Caller->hasFnAttribute(Attribute::Flatten)) {
|
||||
auto IsViable = isInlineViable(*Callee);
|
||||
if (IsViable.isSuccess())
|
||||
return InlineResult::success();
|
||||
return InlineResult::failure(IsViable.getFailureReason());
|
||||
}
|
||||
|
||||
// Don't inline this call if the caller has the optnone attribute.
|
||||
if (Caller->hasOptNone())
|
||||
return InlineResult::failure("optnone attribute");
|
||||
|
||||
@ -2113,6 +2113,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
|
||||
return Attribute::ElementType;
|
||||
case bitc::ATTR_KIND_FNRETTHUNK_EXTERN:
|
||||
return Attribute::FnRetThunkExtern;
|
||||
case bitc::ATTR_KIND_FLATTEN:
|
||||
return Attribute::Flatten;
|
||||
case bitc::ATTR_KIND_INLINE_HINT:
|
||||
return Attribute::InlineHint;
|
||||
case bitc::ATTR_KIND_IN_REG:
|
||||
|
||||
@ -779,6 +779,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
|
||||
return bitc::ATTR_KIND_DISABLE_SANITIZER_INSTRUMENTATION;
|
||||
case Attribute::FnRetThunkExtern:
|
||||
return bitc::ATTR_KIND_FNRETTHUNK_EXTERN;
|
||||
case Attribute::Flatten:
|
||||
return bitc::ATTR_KIND_FLATTEN;
|
||||
case Attribute::Hot:
|
||||
return bitc::ATTR_KIND_HOT;
|
||||
case Attribute::ElementType:
|
||||
|
||||
@ -19,6 +19,8 @@
|
||||
#include "llvm/Analysis/InlineCost.h"
|
||||
#include "llvm/Analysis/OptimizationRemarkEmitter.h"
|
||||
#include "llvm/Analysis/ProfileSummaryInfo.h"
|
||||
#include "llvm/Analysis/TargetLibraryInfo.h"
|
||||
#include "llvm/Analysis/TargetTransformInfo.h"
|
||||
#include "llvm/IR/Module.h"
|
||||
#include "llvm/InitializePasses.h"
|
||||
#include "llvm/Transforms/Utils/Cloning.h"
|
||||
@ -34,12 +36,49 @@ bool AlwaysInlineImpl(
|
||||
Module &M, bool InsertLifetime, ProfileSummaryInfo &PSI,
|
||||
FunctionAnalysisManager *FAM,
|
||||
function_ref<AssumptionCache &(Function &)> GetAssumptionCache,
|
||||
function_ref<AAResults &(Function &)> GetAAR) {
|
||||
function_ref<AAResults &(Function &)> GetAAR,
|
||||
function_ref<TargetTransformInfo &(Function &)> GetTTI,
|
||||
function_ref<const TargetLibraryInfo &(Function &)> GetTLI) {
|
||||
SmallSetVector<CallBase *, 16> Calls;
|
||||
bool Changed = false;
|
||||
SmallVector<Function *, 16> InlinedComdatFunctions;
|
||||
SmallVector<Function *, 4> NeedFlattening;
|
||||
|
||||
auto TryInline = [&](CallBase &CB, Function &Callee,
|
||||
OptimizationRemarkEmitter &ORE, const char *InlineReason,
|
||||
SmallVectorImpl<CallBase *> *NewCallSites =
|
||||
nullptr) -> bool {
|
||||
Function *Caller = CB.getCaller();
|
||||
DebugLoc DLoc = CB.getDebugLoc();
|
||||
BasicBlock *Block = CB.getParent();
|
||||
|
||||
InlineFunctionInfo IFI(GetAssumptionCache, &PSI);
|
||||
InlineResult Res = InlineFunction(CB, IFI, /*MergeAttributes=*/true,
|
||||
&GetAAR(Callee), InsertLifetime);
|
||||
if (!Res.isSuccess()) {
|
||||
ORE.emit([&]() {
|
||||
return OptimizationRemarkMissed(DEBUG_TYPE, "NotInlined", DLoc, Block)
|
||||
<< "'" << ore::NV("Callee", &Callee) << "' is not inlined into '"
|
||||
<< ore::NV("Caller", Caller)
|
||||
<< "': " << ore::NV("Reason", Res.getFailureReason());
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
emitInlinedIntoBasedOnCost(ORE, DLoc, Block, Callee, *Caller,
|
||||
InlineCost::getAlways(InlineReason),
|
||||
/*ForProfileContext=*/false, DEBUG_TYPE);
|
||||
if (FAM)
|
||||
FAM->invalidate(*Caller, PreservedAnalyses::none());
|
||||
if (NewCallSites)
|
||||
*NewCallSites = std::move(IFI.InlinedCallSites);
|
||||
return true;
|
||||
};
|
||||
|
||||
for (Function &F : make_early_inc_range(M)) {
|
||||
if (F.hasFnAttribute(Attribute::Flatten))
|
||||
NeedFlattening.push_back(&F);
|
||||
|
||||
if (F.isPresplitCoroutine())
|
||||
continue;
|
||||
|
||||
@ -56,38 +95,12 @@ bool AlwaysInlineImpl(
|
||||
Calls.insert(CB);
|
||||
|
||||
for (CallBase *CB : Calls) {
|
||||
Function *Caller = CB->getCaller();
|
||||
OptimizationRemarkEmitter ORE(Caller);
|
||||
DebugLoc DLoc = CB->getDebugLoc();
|
||||
BasicBlock *Block = CB->getParent();
|
||||
|
||||
InlineFunctionInfo IFI(GetAssumptionCache, &PSI, nullptr, nullptr);
|
||||
InlineResult Res = InlineFunction(*CB, IFI, /*MergeAttributes=*/true,
|
||||
&GetAAR(F), InsertLifetime);
|
||||
if (!Res.isSuccess()) {
|
||||
ORE.emit([&]() {
|
||||
return OptimizationRemarkMissed(DEBUG_TYPE, "NotInlined", DLoc, Block)
|
||||
<< "'" << ore::NV("Callee", &F) << "' is not inlined into '"
|
||||
<< ore::NV("Caller", Caller)
|
||||
<< "': " << ore::NV("Reason", Res.getFailureReason());
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
emitInlinedIntoBasedOnCost(
|
||||
ORE, DLoc, Block, F, *Caller,
|
||||
InlineCost::getAlways("always inline attribute"),
|
||||
/*ForProfileContext=*/false, DEBUG_TYPE);
|
||||
|
||||
Changed = true;
|
||||
if (FAM)
|
||||
FAM->invalidate(*Caller, PreservedAnalyses::none());
|
||||
OptimizationRemarkEmitter ORE(CB->getCaller());
|
||||
Changed |= TryInline(*CB, F, ORE, "always inline attribute");
|
||||
}
|
||||
|
||||
F.removeDeadConstantUsers();
|
||||
if (F.hasFnAttribute(Attribute::AlwaysInline) && F.isDefTriviallyDead()) {
|
||||
// Remember to try and delete this function afterward. This allows to call
|
||||
// filterDeadComdatFunctions() only once.
|
||||
if (F.hasComdat()) {
|
||||
InlinedComdatFunctions.push_back(&F);
|
||||
} else {
|
||||
@ -99,6 +112,73 @@ bool AlwaysInlineImpl(
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten functions with the flatten attribute using a local worklist.
|
||||
for (Function *F : NeedFlattening) {
|
||||
SmallVector<std::pair<CallBase *, int>, 16> Worklist;
|
||||
SmallVector<std::pair<Function *, int>, 16> InlineHistory;
|
||||
SmallVector<CallBase *> NewCallSites;
|
||||
OptimizationRemarkEmitter ORE(F);
|
||||
|
||||
// Collect initial calls.
|
||||
for (BasicBlock &BB : *F) {
|
||||
for (Instruction &I : BB) {
|
||||
if (auto *CB = dyn_cast<CallBase>(&I)) {
|
||||
Function *Callee = CB->getCalledFunction();
|
||||
if (!Callee || Callee->isDeclaration())
|
||||
continue;
|
||||
Worklist.push_back({CB, -1});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!Worklist.empty()) {
|
||||
auto Item = Worklist.pop_back_val();
|
||||
CallBase *CB = Item.first;
|
||||
int InlineHistoryID = Item.second;
|
||||
Function *Callee = CB->getCalledFunction();
|
||||
if (!Callee)
|
||||
continue;
|
||||
|
||||
// Detect recursion.
|
||||
if (Callee == F ||
|
||||
inlineHistoryIncludes(Callee, InlineHistoryID, InlineHistory)) {
|
||||
ORE.emit([&]() {
|
||||
return OptimizationRemarkMissed("inline", "NotInlined",
|
||||
CB->getDebugLoc(), CB->getParent())
|
||||
<< "'" << ore::NV("Callee", Callee)
|
||||
<< "' is not inlined into '"
|
||||
<< ore::NV("Caller", CB->getCaller())
|
||||
<< "': recursive call during flattening";
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use getAttributeBasedInliningDecision for all attribute-based checks
|
||||
// including TTI/TLI compatibility and isInlineViable.
|
||||
TargetTransformInfo &CalleeTTI = GetTTI(*Callee);
|
||||
auto Decision =
|
||||
getAttributeBasedInliningDecision(*CB, Callee, CalleeTTI, GetTLI);
|
||||
if (!Decision || !Decision->isSuccess())
|
||||
continue;
|
||||
|
||||
if (!TryInline(*CB, *Callee, ORE, "flatten attribute", &NewCallSites))
|
||||
continue;
|
||||
|
||||
Changed = true;
|
||||
|
||||
// Add new call sites from the inlined function to the worklist.
|
||||
if (!NewCallSites.empty()) {
|
||||
int NewHistoryID = InlineHistory.size();
|
||||
InlineHistory.push_back({Callee, InlineHistoryID});
|
||||
for (CallBase *NewCB : NewCallSites) {
|
||||
Function *NewCallee = NewCB->getCalledFunction();
|
||||
if (NewCallee && !NewCallee->isDeclaration())
|
||||
Worklist.push_back({NewCB, NewHistoryID});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!InlinedComdatFunctions.empty()) {
|
||||
// Now we just have the comdat functions. Filter out the ones whose comdats
|
||||
// are not actually dead.
|
||||
@ -134,9 +214,15 @@ struct AlwaysInlinerLegacyPass : public ModulePass {
|
||||
auto GetAssumptionCache = [&](Function &F) -> AssumptionCache & {
|
||||
return getAnalysis<AssumptionCacheTracker>().getAssumptionCache(F);
|
||||
};
|
||||
auto GetTTI = [&](Function &F) -> TargetTransformInfo & {
|
||||
return getAnalysis<TargetTransformInfoWrapperPass>().getTTI(F);
|
||||
};
|
||||
auto GetTLI = [&](Function &F) -> const TargetLibraryInfo & {
|
||||
return getAnalysis<TargetLibraryInfoWrapperPass>().getTLI(F);
|
||||
};
|
||||
|
||||
return AlwaysInlineImpl(M, InsertLifetime, PSI, /*FAM=*/nullptr,
|
||||
GetAssumptionCache, GetAAR);
|
||||
GetAssumptionCache, GetAAR, GetTTI, GetTLI);
|
||||
}
|
||||
|
||||
static char ID; // Pass identification, replacement for typeid
|
||||
@ -145,6 +231,8 @@ struct AlwaysInlinerLegacyPass : public ModulePass {
|
||||
AU.addRequired<AssumptionCacheTracker>();
|
||||
AU.addRequired<AAResultsWrapperPass>();
|
||||
AU.addRequired<ProfileSummaryInfoWrapperPass>();
|
||||
AU.addRequired<TargetLibraryInfoWrapperPass>();
|
||||
AU.addRequired<TargetTransformInfoWrapperPass>();
|
||||
}
|
||||
};
|
||||
|
||||
@ -156,6 +244,8 @@ INITIALIZE_PASS_BEGIN(AlwaysInlinerLegacyPass, "always-inline",
|
||||
INITIALIZE_PASS_DEPENDENCY(AAResultsWrapperPass)
|
||||
INITIALIZE_PASS_DEPENDENCY(AssumptionCacheTracker)
|
||||
INITIALIZE_PASS_DEPENDENCY(ProfileSummaryInfoWrapperPass)
|
||||
INITIALIZE_PASS_DEPENDENCY(TargetLibraryInfoWrapperPass)
|
||||
INITIALIZE_PASS_DEPENDENCY(TargetTransformInfoWrapperPass)
|
||||
INITIALIZE_PASS_END(AlwaysInlinerLegacyPass, "always-inline",
|
||||
"Inliner for always_inline functions", false, false)
|
||||
|
||||
@ -173,10 +263,16 @@ PreservedAnalyses AlwaysInlinerPass::run(Module &M,
|
||||
auto GetAAR = [&](Function &F) -> AAResults & {
|
||||
return FAM.getResult<AAManager>(F);
|
||||
};
|
||||
auto GetTTI = [&](Function &F) -> TargetTransformInfo & {
|
||||
return FAM.getResult<TargetIRAnalysis>(F);
|
||||
};
|
||||
auto GetTLI = [&](Function &F) -> const TargetLibraryInfo & {
|
||||
return FAM.getResult<TargetLibraryAnalysis>(F);
|
||||
};
|
||||
auto &PSI = MAM.getResult<ProfileSummaryAnalysis>(M);
|
||||
|
||||
bool Changed = AlwaysInlineImpl(M, InsertLifetime, PSI, &FAM,
|
||||
GetAssumptionCache, GetAAR);
|
||||
GetAssumptionCache, GetAAR, GetTTI, GetTLI);
|
||||
if (!Changed)
|
||||
return PreservedAnalyses::all();
|
||||
|
||||
|
||||
@ -142,21 +142,6 @@ static cl::opt<CallSiteFormat::Format> CGSCCInlineReplayFormat(
|
||||
"<Line Number>:<Column Number>.<Discriminator> (default)")),
|
||||
cl::desc("How cgscc inline replay file is formatted"), cl::Hidden);
|
||||
|
||||
/// Return true if the specified inline history ID
|
||||
/// indicates an inline history that includes the specified function.
|
||||
static bool inlineHistoryIncludes(
|
||||
Function *F, int InlineHistoryID,
|
||||
const SmallVectorImpl<std::pair<Function *, int>> &InlineHistory) {
|
||||
while (InlineHistoryID != -1) {
|
||||
assert(unsigned(InlineHistoryID) < InlineHistory.size() &&
|
||||
"Invalid inline history ID");
|
||||
if (InlineHistory[InlineHistoryID].first == F)
|
||||
return true;
|
||||
InlineHistoryID = InlineHistory[InlineHistoryID].second;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
InlineAdvisor &
|
||||
InlinerPass::getAdvisor(const ModuleAnalysisManagerCGSCCProxy::Result &MAM,
|
||||
FunctionAnalysisManager &FAM, Module &M) {
|
||||
|
||||
@ -56,21 +56,6 @@ static cl::opt<bool> CtxProfPromoteAlwaysInline(
|
||||
"promotion for that target. If multiple targets for an indirect "
|
||||
"call site fit this description, they are all promoted."));
|
||||
|
||||
/// Return true if the specified inline history ID
|
||||
/// indicates an inline history that includes the specified function.
|
||||
static bool inlineHistoryIncludes(
|
||||
Function *F, int InlineHistoryID,
|
||||
const SmallVectorImpl<std::pair<Function *, int>> &InlineHistory) {
|
||||
while (InlineHistoryID != -1) {
|
||||
assert(unsigned(InlineHistoryID) < InlineHistory.size() &&
|
||||
"Invalid inline history ID");
|
||||
if (InlineHistory[InlineHistoryID].first == F)
|
||||
return true;
|
||||
InlineHistoryID = InlineHistory[InlineHistoryID].second;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
InlineAdvisor &ModuleInlinerPass::getAdvisor(const ModuleAnalysisManager &MAM,
|
||||
FunctionAnalysisManager &FAM,
|
||||
Module &M) {
|
||||
|
||||
@ -938,6 +938,7 @@ Function *CodeExtractor::constructFunctionDeclaration(
|
||||
case Attribute::AlwaysInline:
|
||||
case Attribute::Cold:
|
||||
case Attribute::DisableSanitizerInstrumentation:
|
||||
case Attribute::Flatten:
|
||||
case Attribute::FnRetThunkExtern:
|
||||
case Attribute::Hot:
|
||||
case Attribute::HybridPatchable:
|
||||
|
||||
@ -3444,3 +3444,16 @@ llvm::InlineResult llvm::InlineFunction(CallBase &CB, InlineFunctionInfo &IFI,
|
||||
|
||||
return Result;
|
||||
}
|
||||
|
||||
bool llvm::inlineHistoryIncludes(
|
||||
Function *F, int InlineHistoryID,
|
||||
ArrayRef<std::pair<Function *, int>> InlineHistory) {
|
||||
while (InlineHistoryID != -1) {
|
||||
assert(unsigned(InlineHistoryID) < InlineHistory.size() &&
|
||||
"Invalid inline history ID");
|
||||
if (InlineHistory[InlineHistoryID].first == F)
|
||||
return true;
|
||||
InlineHistoryID = InlineHistory[InlineHistoryID].second;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -526,6 +526,11 @@ define void @f_no_create_undef_or_poison() nocreateundeforpoison {
|
||||
ret void;
|
||||
}
|
||||
|
||||
; CHECK: define void @f_flatten() [[FLATTEN:#[0-9]+]]
|
||||
define void @f_flatten() flatten {
|
||||
ret void;
|
||||
}
|
||||
|
||||
; CHECK: define void @f87() [[FNRETTHUNKEXTERN:#[0-9]+]]
|
||||
define void @f87() fn_ret_thunk_extern { ret void }
|
||||
|
||||
@ -644,6 +649,7 @@ define void @dead_on_return_sized(ptr dead_on_return(4) %p) {
|
||||
; CHECK: attributes #54 = { sanitize_realtime_blocking }
|
||||
; CHECK: attributes #55 = { sanitize_alloc_token }
|
||||
; CHECK: attributes #56 = { nocreateundeforpoison }
|
||||
; CHECK: attributes [[FLATTEN]] = { flatten }
|
||||
; CHECK: attributes [[FNRETTHUNKEXTERN]] = { fn_ret_thunk_extern }
|
||||
; CHECK: attributes [[SKIPPROFILE]] = { skipprofile }
|
||||
; CHECK: attributes [[OPTDEBUG]] = { optdebug }
|
||||
|
||||
91
llvm/test/Transforms/Inline/AArch64/flatten-sme.ll
Normal file
91
llvm/test/Transforms/Inline/AArch64/flatten-sme.ll
Normal file
@ -0,0 +1,91 @@
|
||||
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
|
||||
; RUN: opt < %s -mtriple=aarch64-unknown-linux-gnu -mattr=+sme -S -passes=always-inline | FileCheck %s --check-prefixes=CHECK,ALWAYS
|
||||
; RUN: opt < %s -mtriple=aarch64-unknown-linux-gnu -mattr=+sme -S -passes=inline | FileCheck %s --check-prefixes=CHECK,INLINE
|
||||
|
||||
; Test that flatten attribute respects ABI restrictions for SME.
|
||||
; Streaming callee cannot be inlined into non-streaming caller.
|
||||
; new_za callee cannot be inlined at all.
|
||||
|
||||
define internal i32 @streaming_callee() "aarch64_pstate_sm_enabled" {
|
||||
; CHECK-LABEL: define internal i32 @streaming_callee(
|
||||
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
|
||||
; CHECK-NEXT: ret i32 42
|
||||
;
|
||||
ret i32 42
|
||||
}
|
||||
|
||||
define internal i32 @new_za_callee() "aarch64_new_za" {
|
||||
; CHECK-LABEL: define internal i32 @new_za_callee(
|
||||
; CHECK-SAME: ) #[[ATTR1:[0-9]+]] {
|
||||
; CHECK-NEXT: ret i32 100
|
||||
;
|
||||
ret i32 100
|
||||
}
|
||||
|
||||
define internal i32 @normal_callee() {
|
||||
; ALWAYS-LABEL: define internal i32 @normal_callee(
|
||||
; ALWAYS-SAME: ) #[[ATTR2:[0-9]+]] {
|
||||
; ALWAYS-NEXT: ret i32 50
|
||||
;
|
||||
ret i32 50
|
||||
}
|
||||
|
||||
; Streaming callee -> non-streaming caller: should NOT be inlined (ABI violation).
|
||||
define i32 @test_streaming_not_inlined() flatten {
|
||||
; ALWAYS-LABEL: define i32 @test_streaming_not_inlined(
|
||||
; ALWAYS-SAME: ) #[[ATTR3:[0-9]+]] {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @streaming_callee()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
; INLINE-LABEL: define i32 @test_streaming_not_inlined(
|
||||
; INLINE-SAME: ) #[[ATTR2:[0-9]+]] {
|
||||
; INLINE-NEXT: [[R:%.*]] = call i32 @streaming_callee()
|
||||
; INLINE-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @streaming_callee()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; new_za callee: should NOT be inlined (ABI violation - callee allocates new ZA).
|
||||
define i32 @test_new_za_not_inlined() flatten {
|
||||
; ALWAYS-LABEL: define i32 @test_new_za_not_inlined(
|
||||
; ALWAYS-SAME: ) #[[ATTR3]] {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @new_za_callee()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
; INLINE-LABEL: define i32 @test_new_za_not_inlined(
|
||||
; INLINE-SAME: ) #[[ATTR2]] {
|
||||
; INLINE-NEXT: [[R:%.*]] = call i32 @new_za_callee()
|
||||
; INLINE-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @new_za_callee()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Streaming caller -> streaming callee: should be inlined (compatible).
|
||||
define i32 @test_streaming_to_streaming() flatten "aarch64_pstate_sm_enabled" {
|
||||
; ALWAYS-LABEL: define i32 @test_streaming_to_streaming(
|
||||
; ALWAYS-SAME: ) #[[ATTR4:[0-9]+]] {
|
||||
; ALWAYS-NEXT: ret i32 42
|
||||
;
|
||||
; INLINE-LABEL: define i32 @test_streaming_to_streaming(
|
||||
; INLINE-SAME: ) #[[ATTR3:[0-9]+]] {
|
||||
; INLINE-NEXT: ret i32 42
|
||||
;
|
||||
%r = call i32 @streaming_callee()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Non-streaming caller -> non-streaming callee: should be inlined.
|
||||
define i32 @test_normal_inlined() flatten {
|
||||
; ALWAYS-LABEL: define i32 @test_normal_inlined(
|
||||
; ALWAYS-SAME: ) #[[ATTR3]] {
|
||||
; ALWAYS-NEXT: ret i32 50
|
||||
;
|
||||
; INLINE-LABEL: define i32 @test_normal_inlined(
|
||||
; INLINE-SAME: ) #[[ATTR2]] {
|
||||
; INLINE-NEXT: ret i32 50
|
||||
;
|
||||
%r = call i32 @normal_callee()
|
||||
ret i32 %r
|
||||
}
|
||||
207
llvm/test/Transforms/Inline/flatten.ll
Normal file
207
llvm/test/Transforms/Inline/flatten.ll
Normal file
@ -0,0 +1,207 @@
|
||||
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --scrub-attributes --version 6
|
||||
; RUN: opt -passes=always-inline -S < %s | FileCheck %s --check-prefixes=CHECK,ALWAYS
|
||||
; RUN: opt -passes=inline -S < %s | FileCheck %s --check-prefixes=CHECK,INLINE
|
||||
; RUN: opt -passes='cgscc(inline<only-mandatory>)' -S < %s | FileCheck %s --check-prefixes=CHECK,MANDATORY
|
||||
; RUN: opt -passes=always-inline -pass-remarks-missed=inline -S %s -o /dev/null 2>&1 | FileCheck --check-prefix=REMARK %s
|
||||
|
||||
; Test that the flatten attribute recursively inlines all calls.
|
||||
|
||||
; Multiple levels are inlined.
|
||||
define internal i32 @leaf() {
|
||||
; ALWAYS-LABEL: define internal i32 @leaf() {
|
||||
; ALWAYS-NEXT: ret i32 42
|
||||
;
|
||||
ret i32 42
|
||||
}
|
||||
|
||||
define internal i32 @middle() {
|
||||
; ALWAYS-LABEL: define internal i32 @middle() {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @leaf()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @leaf()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @test_multilevel() flatten {
|
||||
; CHECK-LABEL: define i32 @test_multilevel(
|
||||
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
|
||||
; CHECK-NEXT: ret i32 42
|
||||
;
|
||||
%r = call i32 @middle()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Functions with invoke are inlined.
|
||||
declare i32 @__gxx_personality_v0(...)
|
||||
declare void @may_throw()
|
||||
|
||||
define internal i32 @callee_with_invoke() personality ptr @__gxx_personality_v0 {
|
||||
; ALWAYS-LABEL: define internal i32 @callee_with_invoke() personality ptr @__gxx_personality_v0 {
|
||||
; ALWAYS-NEXT: [[ENTRY:.*:]]
|
||||
; ALWAYS-NEXT: invoke void @may_throw()
|
||||
; ALWAYS-NEXT: to label %[[CONT:.*]] unwind label %[[LPAD:.*]]
|
||||
; ALWAYS: [[CONT]]:
|
||||
; ALWAYS-NEXT: ret i32 100
|
||||
; ALWAYS: [[LPAD]]:
|
||||
; ALWAYS-NEXT: [[LP:%.*]] = landingpad { ptr, i32 }
|
||||
; ALWAYS-NEXT: cleanup
|
||||
; ALWAYS-NEXT: resume { ptr, i32 } [[LP]]
|
||||
;
|
||||
entry:
|
||||
invoke void @may_throw() to label %cont unwind label %lpad
|
||||
cont:
|
||||
ret i32 100
|
||||
lpad:
|
||||
%lp = landingpad { ptr, i32 } cleanup
|
||||
resume { ptr, i32 } %lp
|
||||
}
|
||||
|
||||
define i32 @test_invoke() flatten personality ptr @__gxx_personality_v0 {
|
||||
; CHECK-LABEL: define i32 @test_invoke(
|
||||
; CHECK-SAME: ) #[[ATTR0]] personality ptr @__gxx_personality_v0 {
|
||||
; CHECK-NEXT: [[ENTRY:.*:]]
|
||||
; CHECK-NEXT: invoke void @may_throw()
|
||||
; CHECK-NEXT: to label %[[CALLEE_WITH_INVOKE_EXIT:.*]] unwind label %[[LPAD_I:.*]]
|
||||
; CHECK: [[LPAD_I]]:
|
||||
; CHECK-NEXT: [[LP_I:%.*]] = landingpad { ptr, i32 }
|
||||
; CHECK-NEXT: cleanup
|
||||
; CHECK-NEXT: resume { ptr, i32 } [[LP_I]]
|
||||
; CHECK: [[CALLEE_WITH_INVOKE_EXIT]]:
|
||||
; CHECK-NEXT: ret i32 100
|
||||
;
|
||||
entry:
|
||||
%r = call i32 @callee_with_invoke()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Declaration without definition is not inlined.
|
||||
declare i32 @external_func()
|
||||
|
||||
define i32 @test_declaration() flatten {
|
||||
; CHECK-LABEL: define i32 @test_declaration(
|
||||
; CHECK-SAME: ) #[[ATTR0]] {
|
||||
; CHECK-NEXT: [[R:%.*]] = call i32 @external_func()
|
||||
; CHECK-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @external_func()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Inlined callee that calls a declaration - the declaration should remain after flattening.
|
||||
define internal i32 @calls_external() {
|
||||
; ALWAYS-LABEL: define internal i32 @calls_external() {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @external_func()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @external_func()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @test_inline_then_declaration() flatten {
|
||||
; CHECK-LABEL: define i32 @test_inline_then_declaration(
|
||||
; CHECK-SAME: ) #[[ATTR0]] {
|
||||
; CHECK-NEXT: [[R_I:%.*]] = call i32 @external_func()
|
||||
; CHECK-NEXT: ret i32 [[R_I]]
|
||||
;
|
||||
%r = call i32 @calls_external()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Indirect calls are not inlined.
|
||||
define internal i32 @target_func() {
|
||||
; CHECK-LABEL: define internal i32 @target_func() {
|
||||
; CHECK-NEXT: ret i32 99
|
||||
;
|
||||
ret i32 99
|
||||
}
|
||||
|
||||
define i32 @test_indirect(ptr %func_ptr) flatten {
|
||||
; CHECK-LABEL: define i32 @test_indirect(
|
||||
; CHECK-SAME: ptr [[FUNC_PTR:%.*]]) #[[ATTR0]] {
|
||||
; CHECK-NEXT: [[R:%.*]] = call i32 [[FUNC_PTR]]()
|
||||
; CHECK-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 %func_ptr()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Direct recursion back to flattened function.
|
||||
; The callee calls the flattened function - should not cause infinite inlining.
|
||||
define internal i32 @calls_flattened_func() {
|
||||
; ALWAYS-LABEL: define internal i32 @calls_flattened_func() {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @test_direct_recursion()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @test_direct_recursion()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @test_direct_recursion() flatten {
|
||||
; The call to calls_flattened_func should be inlined, but the recursive call back
|
||||
; to test_direct_recursion should remain.
|
||||
; CHECK-LABEL: define i32 @test_direct_recursion(
|
||||
; CHECK-SAME: ) #[[ATTR0]] {
|
||||
; CHECK-NEXT: [[R_I:%.*]] = call i32 @test_direct_recursion()
|
||||
; CHECK-NEXT: ret i32 [[R_I]]
|
||||
;
|
||||
%r = call i32 @calls_flattened_func()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Mutual recursion (A calls B, B calls A).
|
||||
; Should inline once but not infinitely.
|
||||
define internal i32 @mutual_a() {
|
||||
; ALWAYS-LABEL: define internal i32 @mutual_a() {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @mutual_b()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
; INLINE-LABEL: define internal i32 @mutual_a() {
|
||||
; INLINE-NEXT: [[R_I:%.*]] = call i32 @mutual_a()
|
||||
; INLINE-NEXT: ret i32 [[R_I]]
|
||||
;
|
||||
; MANDATORY-LABEL: define internal i32 @mutual_a() {
|
||||
; MANDATORY-NEXT: [[R:%.*]] = call i32 @mutual_b()
|
||||
; MANDATORY-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @mutual_b()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define internal i32 @mutual_b() {
|
||||
; ALWAYS-LABEL: define internal i32 @mutual_b() {
|
||||
; ALWAYS-NEXT: [[R:%.*]] = call i32 @mutual_a()
|
||||
; ALWAYS-NEXT: ret i32 [[R]]
|
||||
;
|
||||
; MANDATORY-LABEL: define internal i32 @mutual_b() {
|
||||
; MANDATORY-NEXT: [[R:%.*]] = call i32 @mutual_a()
|
||||
; MANDATORY-NEXT: ret i32 [[R]]
|
||||
;
|
||||
%r = call i32 @mutual_a()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
define i32 @test_mutual_recursion() flatten {
|
||||
; After inlining mutual_a, we get call to mutual_b.
|
||||
; After inlining mutual_b, we get call to mutual_a which should remain (skipped due to inline history).
|
||||
; ALWAYS-LABEL: define i32 @test_mutual_recursion(
|
||||
; ALWAYS-SAME: ) #[[ATTR0]] {
|
||||
; ALWAYS-NEXT: [[R_I1:%.*]] = call i32 @mutual_a()
|
||||
; ALWAYS-NEXT: ret i32 [[R_I1]]
|
||||
;
|
||||
; INLINE-LABEL: define i32 @test_mutual_recursion(
|
||||
; INLINE-SAME: ) #[[ATTR0]] {
|
||||
; INLINE-NEXT: [[R:%.*]] = call i32 @mutual_a()
|
||||
; INLINE-NEXT: ret i32 [[R]]
|
||||
;
|
||||
; MANDATORY-LABEL: define i32 @test_mutual_recursion(
|
||||
; MANDATORY-SAME: ) #[[ATTR0]] {
|
||||
; MANDATORY-NEXT: [[R_I1:%.*]] = call i32 @mutual_a()
|
||||
; MANDATORY-NEXT: ret i32 [[R_I1]]
|
||||
;
|
||||
%r = call i32 @mutual_a()
|
||||
ret i32 %r
|
||||
}
|
||||
|
||||
; Check that optimization remark is emitted for recursive calls during flattening.
|
||||
; REMARK: remark: {{.*}} 'test_direct_recursion' is not inlined into 'test_direct_recursion': recursive call during flattening
|
||||
Loading…
x
Reference in New Issue
Block a user