Skip to content

Commit 47d74ca

Browse files
authored
[FuncAttrs][LTO] Relax norecurse attribute inference during postlink LTO (#158608)
This PR, which supersedes llvm/llvm-project#139943, extends the scenarios where the 'norecurse' attribute can be inferred. Currently, the 'norecurse' attribute is only inferred if all called functions also have this attribute. This change introduces a new pass in the LTO pipeline, run after Whole Program Devirtualization, to broaden the inference criteria. The new pass inspects all functions in the module and sets a flag if any functions are external or have their addresses taken (while ignoring those already marked norecurse). This flag is then used with the existing conditions to enable inference in more cases. This enhancement allows 'norecurse' to be applied in situations where a function calls a recursive function, but is not part of the same recursion chain. For example, foo can now be marked 'norecurse' in the following scenarios: `foo -> callee1 -> callee2 -> callee2` In this case, foo and callee1 can both be marked 'norecurse' because they're not part of the callee2 recursion. Similarly, foo can be marked 'norecurse' here: `foo -> callee1 -> callee2 -> callee1` Here, foo is not part of the callee1 -> callee2 -> callee1 recursion chain, so it can be marked 'norecurse'.
1 parent 9194703 commit 47d74ca

12 files changed

+635
-22
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,6 +1960,7 @@ PassBuilder::buildLTODefaultPipeline(OptimizationLevel Level,
19601960
// is fixed.
19611961
MPM.addPass(WholeProgramDevirtPass(ExportSummary, nullptr));
19621962

1963+
MPM.addPass(NoRecurseLTOInferencePass());
19631964
// Stop here at -O1.
19641965
if (Level == OptimizationLevel::O1) {
19651966
// The LowerTypeTestsPass needs to run to lower type metadata and the

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: 97 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 mayHaveRecursiveCallee(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 (!mayHaveRecursiveCallee(*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,62 @@ 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+
if (!F.hasLocalLinkage() || F.hasAddressTaken()) {
2462+
AnyFunctionsAddressIsTaken = true;
2463+
break;
2464+
}
2465+
}
2466+
2467+
// Run norecurse inference on all RefSCCs in the LazyCallGraph for this
2468+
// module.
2469+
bool Changed = false;
2470+
LazyCallGraph &CG = MAM.getResult<LazyCallGraphAnalysis>(M);
2471+
CG.buildRefSCCs();
2472+
2473+
for (LazyCallGraph::RefSCC &RC : CG.postorder_ref_sccs()) {
2474+
// Skip any RefSCC that is part of a call cycle. A RefSCC containing more
2475+
// than one SCC indicates a recursive relationship involving indirect calls.
2476+
if (RC.size() > 1)
2477+
continue;
2478+
2479+
// RefSCC contains a single-SCC. SCC size > 1 indicates mutually recursive
2480+
// functions. Ex: foo1 -> foo2 -> foo3 -> foo1.
2481+
LazyCallGraph::SCC &S = *RC.begin();
2482+
if (S.size() > 1)
2483+
continue;
2484+
2485+
// Get the single function from this SCC.
2486+
Function &F = S.begin()->getFunction();
2487+
if (!F.hasExactDefinition() || F.doesNotRecurse())
2488+
continue;
2489+
2490+
// If the analysis confirms that this function has no recursive calls
2491+
// (either direct, indirect, or through external linkages),
2492+
// we can safely apply the norecurse attribute.
2493+
if (!mayHaveRecursiveCallee(F, AnyFunctionsAddressIsTaken)) {
2494+
F.setDoesNotRecurse();
2495+
++NumNoRecurse;
2496+
Changed = true;
2497+
}
2498+
}
2499+
2500+
PreservedAnalyses PA;
2501+
if (Changed)
2502+
PA.preserve<LazyCallGraphAnalysis>();
2503+
else
2504+
PA = PreservedAnalyses::all();
2505+
return PA;
2506+
}

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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --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+
;.
11+
; CHECK: @.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
12+
;.
13+
define dso_local void @bob() {
14+
; CHECK-LABEL: define dso_local void @bob() {
15+
; CHECK-NEXT: [[ENTRY:.*:]]
16+
; CHECK-NEXT: [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @.str)
17+
; CHECK-NEXT: ret void
18+
;
19+
entry:
20+
%call = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @.str)
21+
ret void
22+
}
23+
24+
declare i32 @printf(ptr readonly captures(none), ...)
25+
26+
define dso_local i32 @main() norecurse {
27+
; CHECK: Function Attrs: norecurse
28+
; CHECK-LABEL: define dso_local i32 @main(
29+
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
30+
; CHECK-NEXT: [[ENTRY:.*:]]
31+
; CHECK-NEXT: tail call void @bob()
32+
; CHECK-NEXT: ret i32 0
33+
;
34+
entry:
35+
tail call void @bob()
36+
ret i32 0
37+
}
38+
;.
39+
; CHECK: attributes #[[ATTR0]] = { norecurse }
40+
;.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --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+
;.
12+
; CHECK: @.str = private unnamed_addr constant [12 x i8] c"Hello World\00", align 1
13+
;.
14+
define internal void @bob() {
15+
; CHECK: Function Attrs: norecurse
16+
; CHECK-LABEL: define internal void @bob(
17+
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
18+
; CHECK-NEXT: [[ENTRY:.*:]]
19+
; CHECK-NEXT: [[CALL:%.*]] = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @.str)
20+
; CHECK-NEXT: ret void
21+
;
22+
entry:
23+
%call = tail call i32 (ptr, ...) @printf(ptr nonnull dereferenceable(1) @.str)
24+
ret void
25+
}
26+
27+
; Function Attrs: nofree nounwind
28+
declare i32 @printf(ptr readonly captures(none), ...)
29+
30+
; Function Attrs: nofree norecurse nounwind uwtable
31+
define dso_local i32 @main() norecurse {
32+
; CHECK: Function Attrs: norecurse
33+
; CHECK-LABEL: define dso_local i32 @main(
34+
; CHECK-SAME: ) #[[ATTR0]] {
35+
; CHECK-NEXT: [[ENTRY:.*:]]
36+
; CHECK-NEXT: tail call void @bob()
37+
; CHECK-NEXT: ret i32 0
38+
;
39+
entry:
40+
tail call void @bob()
41+
ret i32 0
42+
}
43+
;.
44+
; CHECK: attributes #[[ATTR0]] = { norecurse }
45+
;.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-attributes --check-globals all --version 5
2+
; RUN: opt < %s -passes=norecurse-lto-inference -S | FileCheck %s
3+
4+
; This test includes a call graph which has a recursive function(foo2) which
5+
; calls a non-recursive internal function (foo3) satisfying the norecurse
6+
; attribute criteria.
7+
8+
9+
define internal void @foo3() {
10+
; CHECK: Function Attrs: norecurse
11+
; CHECK-LABEL: define internal void @foo3(
12+
; CHECK-SAME: ) #[[ATTR0:[0-9]+]] {
13+
; CHECK-NEXT: ret void
14+
;
15+
ret void
16+
}
17+
18+
define internal i32 @foo2(i32 %accum, i32 %n) {
19+
; CHECK-LABEL: define internal i32 @foo2(
20+
; CHECK-SAME: i32 [[ACCUM:%.*]], i32 [[N:%.*]]) {
21+
; CHECK-NEXT: [[ENTRY:.*]]:
22+
; CHECK-NEXT: [[CMP:%.*]] = icmp eq i32 [[N]], 0
23+
; CHECK-NEXT: br i1 [[CMP]], label %[[EXIT:.*]], label %[[RECURSE:.*]]
24+
; CHECK: [[RECURSE]]:
25+
; CHECK-NEXT: [[SUB:%.*]] = sub i32 [[N]], 1
26+
; CHECK-NEXT: [[MUL:%.*]] = mul i32 [[ACCUM]], [[SUB]]
27+
; CHECK-NEXT: [[CALL:%.*]] = call i32 @foo2(i32 [[MUL]], i32 [[SUB]])
28+
; CHECK-NEXT: call void @foo3()
29+
; CHECK-NEXT: br label %[[EXIT]]
30+
; CHECK: [[EXIT]]:
31+
; CHECK-NEXT: [[RES:%.*]] = phi i32 [ [[ACCUM]], %[[ENTRY]] ], [ [[CALL]], %[[RECURSE]] ]
32+
; CHECK-NEXT: ret i32 [[RES]]
33+
;
34+
entry:
35+
%cmp = icmp eq i32 %n, 0
36+
br i1 %cmp, label %exit, label %recurse
37+
38+
recurse:
39+
%sub = sub i32 %n, 1
40+
%mul = mul i32 %accum, %sub
41+
%call = call i32 @foo2(i32 %mul, i32 %sub)
42+
call void @foo3()
43+
br label %exit
44+
45+
exit:
46+
%res = phi i32 [ %accum, %entry ], [ %call, %recurse ]
47+
ret i32 %res
48+
}
49+
50+
define internal i32 @foo1() {
51+
; CHECK-LABEL: define internal i32 @foo1() {
52+
; CHECK-NEXT: [[RES:%.*]] = call i32 @foo2(i32 1, i32 5)
53+
; CHECK-NEXT: ret i32 [[RES]]
54+
;
55+
%res = call i32 @foo2(i32 1, i32 5)
56+
ret i32 %res
57+
}
58+
59+
define dso_local i32 @main() {
60+
; CHECK-LABEL: define dso_local i32 @main() {
61+
; CHECK-NEXT: [[RES:%.*]] = call i32 @foo1()
62+
; CHECK-NEXT: ret i32 [[RES]]
63+
;
64+
%res = call i32 @foo1()
65+
ret i32 %res
66+
}
67+
;.
68+
; CHECK: attributes #[[ATTR0]] = { norecurse }
69+
;.

0 commit comments

Comments
 (0)