Skip to content

Commit 09220c5

Browse files
committed
[FuncAttrs][LTO] Relax norecurse attribute inference during postlink LTO
1 parent bd7c2f1 commit 09220c5

10 files changed

+559
-23
lines changed

llvm/include/llvm/Transforms/IPO/FunctionAttrs.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,19 @@ class ReversePostOrderFunctionAttrsPass
7979
LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
8080
};
8181

82+
/// Additional 'norecurse' attribute deduction during postlink LTO phase.
83+
///
84+
/// This is a module pass that infers 'norecurse' attribute on functions.
85+
/// It runs during LTO and analyzes the module's call graph to find functions
86+
/// that are guaranteed not to call themselves, either directly or indirectly.
87+
/// The pass uses a module-wide flag which checks if any function's address is
88+
/// taken or any function in the module has external linkage, to safely handle
89+
/// indirect and library function calls from current function.
90+
class NoRecurseLTOInferencePass
91+
: public PassInfoMixin<NoRecurseLTOInferencePass> {
92+
public:
93+
LLVM_ABI PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM);
94+
};
8295
} // end namespace llvm
8396

8497
#endif // LLVM_TRANSFORMS_IPO_FUNCTIONATTRS_H

llvm/lib/Passes/PassBuilderPipelines.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,7 @@ PassBuilder::buildLTODefaultPipeline(OptimizationLevel Level,
19441944
// is fixed.
19451945
MPM.addPass(WholeProgramDevirtPass(ExportSummary, nullptr));
19461946

1947+
MPM.addPass(NoRecurseLTOInferencePass());
19471948
// Stop here at -O1.
19481949
if (Level == OptimizationLevel::O1) {
19491950
// The LowerTypeTestsPass needs to run to lower type metadata and the
@@ -2355,4 +2356,4 @@ AAManager PassBuilder::buildDefaultAAPipeline() {
23552356
bool PassBuilder::isInstrumentedPGOUse() const {
23562357
return (PGOOpt && PGOOpt->Action == PGOOptions::IRUse) ||
23572358
!UseCtxProfile.empty();
2358-
}
2359+
}

llvm/lib/Passes/PassRegistry.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ MODULE_PASS("metarenamer", MetaRenamerPass())
119119
MODULE_PASS("module-inline", ModuleInlinerPass())
120120
MODULE_PASS("name-anon-globals", NameAnonGlobalPass())
121121
MODULE_PASS("no-op-module", NoOpModulePass())
122+
MODULE_PASS("norecurse-lto-inference", NoRecurseLTOInferencePass())
122123
MODULE_PASS("nsan", NumericalStabilitySanitizerPass())
123124
MODULE_PASS("openmp-opt", OpenMPOptPass())
124125
MODULE_PASS("openmp-opt-postlink",

llvm/lib/Transforms/IPO/FunctionAttrs.cpp

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,6 +2067,36 @@ static void inferAttrsFromFunctionBodies(const SCCNodeSet &SCCNodes,
20672067
AI.run(SCCNodes, Changed);
20682068
}
20692069

2070+
// Determines if the function 'F' can be marked 'norecurse'.
2071+
// It returns true if any call within 'F' could lead to a recursive
2072+
// call back to 'F', and false otherwise.
2073+
// The 'AnyFunctionsAddressIsTaken' parameter is a module-wide flag
2074+
// that is true if any function's address is taken, or if any function
2075+
// has external linkage. This is used to determine the safety of
2076+
// external/library calls.
2077+
static bool hasRecursiveCallee(Function &F,
2078+
bool AnyFunctionsAddressIsTaken = true) {
2079+
for (const auto &BB : F) {
2080+
for (const auto &I : BB.instructionsWithoutDebug()) {
2081+
if (const auto *CB = dyn_cast<CallBase>(&I)) {
2082+
const Function *Callee = CB->getCalledFunction();
2083+
if (!Callee || Callee == &F)
2084+
return true;
2085+
2086+
if (Callee->doesNotRecurse())
2087+
continue;
2088+
2089+
if (!AnyFunctionsAddressIsTaken ||
2090+
(Callee->isDeclaration() &&
2091+
Callee->hasFnAttribute(Attribute::NoCallback)))
2092+
continue;
2093+
return true;
2094+
}
2095+
}
2096+
}
2097+
return false;
2098+
}
2099+
20702100
static void addNoRecurseAttrs(const SCCNodeSet &SCCNodes,
20712101
SmallPtrSet<Function *, 8> &Changed) {
20722102
// Try and identify functions that do not recurse.
@@ -2078,28 +2108,14 @@ static void addNoRecurseAttrs(const SCCNodeSet &SCCNodes,
20782108
Function *F = *SCCNodes.begin();
20792109
if (!F || !F->hasExactDefinition() || F->doesNotRecurse())
20802110
return;
2081-
2082-
// If all of the calls in F are identifiable and are to norecurse functions, F
2083-
// is norecurse. This check also detects self-recursion as F is not currently
2084-
// marked norecurse, so any called from F to F will not be marked norecurse.
2085-
for (auto &BB : *F)
2086-
for (auto &I : BB.instructionsWithoutDebug())
2087-
if (auto *CB = dyn_cast<CallBase>(&I)) {
2088-
Function *Callee = CB->getCalledFunction();
2089-
if (!Callee || Callee == F ||
2090-
(!Callee->doesNotRecurse() &&
2091-
!(Callee->isDeclaration() &&
2092-
Callee->hasFnAttribute(Attribute::NoCallback))))
2093-
// Function calls a potentially recursive function.
2094-
return;
2095-
}
2096-
2097-
// Every call was to a non-recursive function other than this function, and
2098-
// we have no indirect recursion as the SCC size is one. This function cannot
2099-
// recurse.
2100-
F->setDoesNotRecurse();
2101-
++NumNoRecurse;
2102-
Changed.insert(F);
2111+
if (!hasRecursiveCallee(*F)) {
2112+
// Every call was to a non-recursive function other than this function, and
2113+
// we have no indirect recursion as the SCC size is one. This function
2114+
// cannot recurse.
2115+
F->setDoesNotRecurse();
2116+
++NumNoRecurse;
2117+
Changed.insert(F);
2118+
}
21032119
}
21042120

21052121
// Set the noreturn function attribute if possible.
@@ -2429,3 +2445,66 @@ ReversePostOrderFunctionAttrsPass::run(Module &M, ModuleAnalysisManager &AM) {
24292445
PA.preserve<LazyCallGraphAnalysis>();
24302446
return PA;
24312447
}
2448+
2449+
PreservedAnalyses NoRecurseLTOInferencePass::run(Module &M,
2450+
ModuleAnalysisManager &MAM) {
2451+
2452+
// Check if any function in the whole program has its address taken or has
2453+
// potentially external linkage.
2454+
// We use this information when inferring norecurse attribute: If there is
2455+
// no function whose address is taken and all functions have internal
2456+
// linkage, there is no path for a callback to any user function.
2457+
bool AnyFunctionsAddressIsTaken = false;
2458+
for (Function &F : M) {
2459+
if (F.isDeclaration() || F.doesNotRecurse()) {
2460+
continue;
2461+
}
2462+
if (!F.hasLocalLinkage() || F.hasAddressTaken()) {
2463+
AnyFunctionsAddressIsTaken = true;
2464+
break;
2465+
}
2466+
}
2467+
2468+
// Run norecurse inference on all RefSCCs in the LazyCallGraph for this
2469+
// module.
2470+
bool Changed = false;
2471+
LazyCallGraph &CG = MAM.getResult<LazyCallGraphAnalysis>(M);
2472+
CG.buildRefSCCs();
2473+
2474+
for (LazyCallGraph::RefSCC &RC : CG.postorder_ref_sccs()) {
2475+
// Skip any RefSCC that is part of a call cycle. A RefSCC containing more
2476+
// than one SCC indicates a recursive relationship, which could involve
2477+
// direct or indirect calls.
2478+
if (RC.size() > 1) {
2479+
continue;
2480+
}
2481+
2482+
// A single-SCC RefSCC could still be a self-loop.
2483+
LazyCallGraph::SCC &S = *RC.begin();
2484+
if (S.size() > 1) {
2485+
continue;
2486+
}
2487+
2488+
// Get the single function from this SCC.
2489+
Function &F = S.begin()->getFunction();
2490+
if (!F.hasExactDefinition() || F.doesNotRecurse()) {
2491+
continue;
2492+
}
2493+
2494+
// If the analysis confirms that this function has no recursive calls
2495+
// (either direct, indirect, or through external linkages),
2496+
// we can safely apply the norecurse attribute.
2497+
if (!hasRecursiveCallee(F, AnyFunctionsAddressIsTaken)) {
2498+
F.setDoesNotRecurse();
2499+
++NumNoRecurse;
2500+
Changed = true;
2501+
}
2502+
}
2503+
2504+
PreservedAnalyses PA;
2505+
if (Changed)
2506+
PA.preserve<LazyCallGraphAnalysis>();
2507+
else
2508+
PA = PreservedAnalyses::all();
2509+
return PA;
2510+
}

llvm/test/Other/new-pm-lto-defaults.ll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
; CHECK-O1-NEXT: Running analysis: TargetLibraryAnalysis
6868
; CHECK-O-NEXT: Running pass: GlobalSplitPass
6969
; CHECK-O-NEXT: Running pass: WholeProgramDevirtPass
70+
; CHECK-O-NEXT: Running pass: NoRecurseLTOInferencePass
7071
; CHECK-O23SZ-NEXT: Running pass: CoroEarlyPass
7172
; CHECK-O1-NEXT: Running pass: LowerTypeTestsPass
7273
; CHECK-O23SZ-NEXT: Running pass: GlobalOptPass
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
2+
; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
3+
4+
; This test includes a call to a library function which is not marked as
5+
; NoCallback. Function bob() does not have internal linkage and hence prevents
6+
; norecurse to be added.
7+
8+
@.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
9+
10+
define dso_local void @bob() {
11+
; CHECK-LABEL: define dso_local void @bob() {
12+
; CHECK-NEXT: [[ENTRY:.*:]]
13+
; CHECK-NEXT: [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
14+
; CHECK-NEXT: ret void
15+
;
16+
entry:
17+
%call = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
18+
ret void
19+
}
20+
21+
declare noundef i32 @printf(ptr noundef readonly captures(none), ...)
22+
23+
define dso_local noundef i32 @main() norecurse {
24+
; CHECK: Function Attrs: norecurse
25+
; CHECK-LABEL: define dso_local noundef i32 @main(
26+
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
27+
; CHECK-NEXT: [[ENTRY:.*:]]
28+
; CHECK-NEXT: tail call void @bob()
29+
; CHECK-NEXT: ret i32 0
30+
;
31+
entry:
32+
tail call void @bob()
33+
ret i32 0
34+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --version 5
2+
; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
3+
4+
; This test includes a call to a library function which is not marked as
5+
; NoCallback. All functions except main() are internal and main is marked
6+
; norecurse, so as to not block norecurse to be added to bob().
7+
8+
@.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
9+
10+
; Function Attrs: nofree noinline nounwind uwtable
11+
define internal void @bob() {
12+
; CHECK: Function Attrs: norecurse
13+
; CHECK-LABEL: define internal void @bob(
14+
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
15+
; CHECK-NEXT: [[ENTRY:.*:]]
16+
; CHECK-NEXT: [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
17+
; CHECK-NEXT: ret void
18+
;
19+
entry:
20+
%call = tail call i32 (ptr, ...) @printf(ptr noundef nonnull dereferenceable(1) @.str)
21+
ret void
22+
}
23+
24+
; Function Attrs: nofree nounwind
25+
declare noundef i32 @printf(ptr noundef readonly captures(none), ...)
26+
27+
; Function Attrs: nofree norecurse nounwind uwtable
28+
define dso_local noundef i32 @main() norecurse {
29+
; CHECK: Function Attrs: norecurse
30+
; CHECK-LABEL: define dso_local noundef i32 @main(
31+
; CHECK-SAME: ) #[[ATTR0]] {
32+
; CHECK-NEXT: [[ENTRY:.*:]]
33+
; CHECK-NEXT: tail call void @bob()
34+
; CHECK-NEXT: ret i32 0
35+
;
36+
entry:
37+
tail call void @bob()
38+
ret i32 0
39+
}

0 commit comments

Comments
 (0)