Skip to content

Commit 8fca65c

Browse files
authored
[LifetimeSafety] Optimize loan propagation by separating persistent and block-local origins (#165789)
## Summary Optimizes the lifetime analysis loan propagation by preventing block-local origins from participating in expensive join operations at block boundaries. ## Problem The lifetime analysis currently performs join operations on all origins at every block boundary. However, many origins are block-local: they exist only to propagate loans between persistent origins across temporary declarations and expressions within a single block. Including them in joins is unnecessary and expensive. ## Solution This PR classifies origins into two categories: - **Persistent origins**: referenced in multiple basic blocks, must participate in joins - **Block-local origins**: confined to a single block, can be discarded at block boundaries ### Implementation 1. **Pre-pass** (`computePersistentOrigins`): Analyzes all facts in the CFG to identify which origins appear in multiple blocks 2. **Split lattice**: Maintains two separate `OriginLoanMap`s: - `PersistentOrigins`: participates in join operations - `BlockLocalOrigins`: used during block execution, discarded at boundaries 3. **Optimized join**: Only merges persistent origins; block-local map is reset to empty ### Benefits - Significantly reduces join complexity, especially in functions with many temporary locals - Performance scales with the ratio of temporary to persistent origins - Correctness preserved: block-local loan propagation still works within blocks Fixes: #165780 Fixes: #164625. It brings down regression from **150% to 2%**. ---
1 parent f2857c2 commit 8fca65c

File tree

7 files changed

+132
-29
lines changed

7 files changed

+132
-29
lines changed

clang/include/clang/Analysis/Analyses/LifetimeSafety/Facts.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,19 +144,18 @@ class ReturnOfOriginFact : public Fact {
144144

145145
class UseFact : public Fact {
146146
const Expr *UseExpr;
147+
OriginID OID;
147148
// True if this use is a write operation (e.g., left-hand side of assignment).
148149
// Write operations are exempted from use-after-free checks.
149150
bool IsWritten = false;
150151

151152
public:
152153
static bool classof(const Fact *F) { return F->getKind() == Kind::Use; }
153154

154-
UseFact(const Expr *UseExpr) : Fact(Kind::Use), UseExpr(UseExpr) {}
155+
UseFact(const Expr *UseExpr, OriginManager &OM)
156+
: Fact(Kind::Use), UseExpr(UseExpr), OID(OM.get(*UseExpr)) {}
155157

156-
OriginID getUsedOrigin(const OriginManager &OM) const {
157-
// TODO: Remove const cast and make OriginManager::get as const.
158-
return const_cast<OriginManager &>(OM).get(*UseExpr);
159-
}
158+
OriginID getUsedOrigin() const { return OID; }
160159
const Expr *getUseExpr() const { return UseExpr; }
161160
void markAsWritten() { IsWritten = true; }
162161
bool isWritten() const { return IsWritten; }

clang/include/clang/Analysis/Analyses/LifetimeSafety/Origins.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ class OriginManager {
7474

7575
OriginID getOrCreate(const ValueDecl &D);
7676

77+
unsigned getNumOrigins() const { return NextOriginID.Value; }
78+
7779
void dump(OriginID OID, llvm::raw_ostream &OS) const;
7880

7981
private:

clang/lib/Analysis/LifetimeSafety/Facts.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ void ReturnOfOriginFact::dump(llvm::raw_ostream &OS, const LoanManager &,
5353
void UseFact::dump(llvm::raw_ostream &OS, const LoanManager &,
5454
const OriginManager &OM) const {
5555
OS << "Use (";
56-
OM.dump(getUsedOrigin(OM), OS);
56+
OM.dump(getUsedOrigin(), OS);
5757
OS << ", " << (isWritten() ? "Write" : "Read") << ")\n";
5858
}
5959

clang/lib/Analysis/LifetimeSafety/FactsGenerator.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ void FactsGenerator::handleAssignment(const Expr *LHSExpr,
333333
// (e.g. on the left-hand side of an assignment).
334334
void FactsGenerator::handleUse(const DeclRefExpr *DRE) {
335335
if (isPointerType(DRE->getType())) {
336-
UseFact *UF = FactMgr.createFact<UseFact>(DRE);
336+
UseFact *UF = FactMgr.createFact<UseFact>(DRE, FactMgr.getOriginMgr());
337337
CurrentBlockFacts.push_back(UF);
338338
assert(!UseFacts.contains(DRE));
339339
UseFacts[DRE] = UF;

clang/lib/Analysis/LifetimeSafety/LiveOrigins.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class AnalysisImpl
111111
/// dominates this program point. A write operation kills the liveness of
112112
/// the origin since it overwrites the value.
113113
Lattice transfer(Lattice In, const UseFact &UF) {
114-
OriginID OID = UF.getUsedOrigin(FactMgr.getOriginMgr());
114+
OriginID OID = UF.getUsedOrigin();
115115
// Write kills liveness.
116116
if (UF.isWritten())
117117
return Lattice(Factory.remove(In.LiveOrigins, OID));

clang/lib/Analysis/LifetimeSafety/LoanPropagation.cpp

Lines changed: 116 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,114 @@
55
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
66
//
77
//===----------------------------------------------------------------------===//
8-
#include "clang/Analysis/Analyses/LifetimeSafety/LoanPropagation.h"
9-
#include "Dataflow.h"
8+
#include <cassert>
109
#include <memory>
1110

11+
#include "Dataflow.h"
12+
#include "clang/Analysis/Analyses/LifetimeSafety/Facts.h"
13+
#include "clang/Analysis/Analyses/LifetimeSafety/LoanPropagation.h"
14+
#include "clang/Analysis/Analyses/LifetimeSafety/Loans.h"
15+
#include "clang/Analysis/Analyses/LifetimeSafety/Origins.h"
16+
#include "clang/Analysis/Analyses/LifetimeSafety/Utils.h"
17+
#include "clang/Analysis/AnalysisDeclContext.h"
18+
#include "clang/Analysis/CFG.h"
19+
#include "clang/Basic/LLVM.h"
20+
#include "llvm/ADT/BitVector.h"
21+
#include "llvm/ADT/SmallVector.h"
22+
#include "llvm/Support/TimeProfiler.h"
23+
#include "llvm/Support/raw_ostream.h"
24+
1225
namespace clang::lifetimes::internal {
26+
27+
// Prepass to find persistent origins. An origin is persistent if it is
28+
// referenced in more than one basic block.
29+
static llvm::BitVector computePersistentOrigins(const FactManager &FactMgr,
30+
const CFG &C) {
31+
llvm::TimeTraceScope("ComputePersistentOrigins");
32+
unsigned NumOrigins = FactMgr.getOriginMgr().getNumOrigins();
33+
llvm::BitVector PersistentOrigins(NumOrigins);
34+
35+
llvm::SmallVector<const CFGBlock *> OriginToFirstSeenBlock(NumOrigins,
36+
nullptr);
37+
for (const CFGBlock *B : C) {
38+
for (const Fact *F : FactMgr.getFacts(B)) {
39+
auto CheckOrigin = [&](OriginID OID) {
40+
if (PersistentOrigins.test(OID.Value))
41+
return;
42+
auto &FirstSeenBlock = OriginToFirstSeenBlock[OID.Value];
43+
if (FirstSeenBlock == nullptr)
44+
FirstSeenBlock = B;
45+
if (FirstSeenBlock != B) {
46+
// We saw this origin in more than one block.
47+
PersistentOrigins.set(OID.Value);
48+
}
49+
};
50+
51+
switch (F->getKind()) {
52+
case Fact::Kind::Issue:
53+
CheckOrigin(F->getAs<IssueFact>()->getOriginID());
54+
break;
55+
case Fact::Kind::OriginFlow: {
56+
const auto *OF = F->getAs<OriginFlowFact>();
57+
CheckOrigin(OF->getDestOriginID());
58+
CheckOrigin(OF->getSrcOriginID());
59+
break;
60+
}
61+
case Fact::Kind::ReturnOfOrigin:
62+
CheckOrigin(F->getAs<ReturnOfOriginFact>()->getReturnedOriginID());
63+
break;
64+
case Fact::Kind::Use:
65+
CheckOrigin(F->getAs<UseFact>()->getUsedOrigin());
66+
break;
67+
case Fact::Kind::Expire:
68+
case Fact::Kind::TestPoint:
69+
break;
70+
}
71+
}
72+
}
73+
return PersistentOrigins;
74+
}
75+
1376
namespace {
77+
1478
/// Represents the dataflow lattice for loan propagation.
1579
///
1680
/// This lattice tracks which loans each origin may hold at a given program
1781
/// point.The lattice has a finite height: An origin's loan set is bounded by
1882
/// the total number of loans in the function.
19-
/// TODO(opt): To reduce the lattice size, propagate origins of declarations,
20-
/// not expressions, because expressions are not visible across blocks.
2183
struct Lattice {
2284
/// The map from an origin to the set of loans it contains.
23-
OriginLoanMap Origins = OriginLoanMap(nullptr);
24-
25-
explicit Lattice(const OriginLoanMap &S) : Origins(S) {}
85+
/// Origins that appear in multiple blocks. Participates in join operations.
86+
OriginLoanMap PersistentOrigins = OriginLoanMap(nullptr);
87+
/// Origins confined to a single block. Discarded at block boundaries.
88+
OriginLoanMap BlockLocalOrigins = OriginLoanMap(nullptr);
89+
90+
explicit Lattice(const OriginLoanMap &Persistent,
91+
const OriginLoanMap &BlockLocal)
92+
: PersistentOrigins(Persistent), BlockLocalOrigins(BlockLocal) {}
2693
Lattice() = default;
2794

2895
bool operator==(const Lattice &Other) const {
29-
return Origins == Other.Origins;
96+
return PersistentOrigins == Other.PersistentOrigins &&
97+
BlockLocalOrigins == Other.BlockLocalOrigins;
3098
}
3199
bool operator!=(const Lattice &Other) const { return !(*this == Other); }
32100

33101
void dump(llvm::raw_ostream &OS) const {
34102
OS << "LoanPropagationLattice State:\n";
35-
if (Origins.isEmpty())
103+
OS << " Persistent Origins:\n";
104+
if (PersistentOrigins.isEmpty())
36105
OS << " <empty>\n";
37-
for (const auto &Entry : Origins) {
106+
for (const auto &Entry : PersistentOrigins) {
107+
if (Entry.second.isEmpty())
108+
OS << " Origin " << Entry.first << " contains no loans\n";
109+
for (const LoanID &LID : Entry.second)
110+
OS << " Origin " << Entry.first << " contains Loan " << LID << "\n";
111+
}
112+
OS << " Block-Local Origins:\n";
113+
if (BlockLocalOrigins.isEmpty())
114+
OS << " <empty>\n";
115+
for (const auto &Entry : BlockLocalOrigins) {
38116
if (Entry.second.isEmpty())
39117
OS << " Origin " << Entry.first << " contains no loans\n";
40118
for (const LoanID &LID : Entry.second)
@@ -50,7 +128,8 @@ class AnalysisImpl
50128
OriginLoanMap::Factory &OriginLoanMapFactory,
51129
LoanSet::Factory &LoanSetFactory)
52130
: DataflowAnalysis(C, AC, F), OriginLoanMapFactory(OriginLoanMapFactory),
53-
LoanSetFactory(LoanSetFactory) {}
131+
LoanSetFactory(LoanSetFactory),
132+
PersistentOrigins(computePersistentOrigins(F, C)) {}
54133

55134
using Base::transfer;
56135

@@ -59,10 +138,10 @@ class AnalysisImpl
59138
Lattice getInitialState() { return Lattice{}; }
60139

61140
/// Merges two lattices by taking the union of loans for each origin.
62-
// TODO(opt): Keep the state small by removing origins which become dead.
141+
/// Only persistent origins are joined; block-local origins are discarded.
63142
Lattice join(Lattice A, Lattice B) {
64143
OriginLoanMap JoinedOrigins = utils::join(
65-
A.Origins, B.Origins, OriginLoanMapFactory,
144+
A.PersistentOrigins, B.PersistentOrigins, OriginLoanMapFactory,
66145
[&](const LoanSet *S1, const LoanSet *S2) {
67146
assert((S1 || S2) && "unexpectedly merging 2 empty sets");
68147
if (!S1)
@@ -74,16 +153,15 @@ class AnalysisImpl
74153
// Asymmetric join is a performance win. For origins present only on one
75154
// branch, the loan set can be carried over as-is.
76155
utils::JoinKind::Asymmetric);
77-
return Lattice(JoinedOrigins);
156+
return Lattice(JoinedOrigins, OriginLoanMapFactory.getEmptyMap());
78157
}
79158

80159
/// A new loan is issued to the origin. Old loans are erased.
81160
Lattice transfer(Lattice In, const IssueFact &F) {
82161
OriginID OID = F.getOriginID();
83162
LoanID LID = F.getLoanID();
84-
return Lattice(OriginLoanMapFactory.add(
85-
In.Origins, OID,
86-
LoanSetFactory.add(LoanSetFactory.getEmptySet(), LID)));
163+
LoanSet NewLoans = LoanSetFactory.add(LoanSetFactory.getEmptySet(), LID);
164+
return setLoans(In, OID, NewLoans);
87165
}
88166

89167
/// A flow from source to destination. If `KillDest` is true, this replaces
@@ -98,22 +176,41 @@ class AnalysisImpl
98176
LoanSet SrcLoans = getLoans(In, SrcOID);
99177
LoanSet MergedLoans = utils::join(DestLoans, SrcLoans, LoanSetFactory);
100178

101-
return Lattice(OriginLoanMapFactory.add(In.Origins, DestOID, MergedLoans));
179+
return setLoans(In, DestOID, MergedLoans);
102180
}
103181

104182
LoanSet getLoans(OriginID OID, ProgramPoint P) const {
105183
return getLoans(getState(P), OID);
106184
}
107185

108186
private:
187+
/// Returns true if the origin is persistent (referenced in multiple blocks).
188+
bool isPersistent(OriginID OID) const {
189+
return PersistentOrigins.test(OID.Value);
190+
}
191+
192+
Lattice setLoans(Lattice L, OriginID OID, LoanSet Loans) {
193+
if (isPersistent(OID))
194+
return Lattice(OriginLoanMapFactory.add(L.PersistentOrigins, OID, Loans),
195+
L.BlockLocalOrigins);
196+
return Lattice(L.PersistentOrigins,
197+
OriginLoanMapFactory.add(L.BlockLocalOrigins, OID, Loans));
198+
}
199+
109200
LoanSet getLoans(Lattice L, OriginID OID) const {
110-
if (auto *Loans = L.Origins.lookup(OID))
201+
const OriginLoanMap *Map =
202+
isPersistent(OID) ? &L.PersistentOrigins : &L.BlockLocalOrigins;
203+
if (auto *Loans = Map->lookup(OID))
111204
return *Loans;
112205
return LoanSetFactory.getEmptySet();
113206
}
114207

115208
OriginLoanMap::Factory &OriginLoanMapFactory;
116209
LoanSet::Factory &LoanSetFactory;
210+
/// Boolean vector indexed by origin ID. If true, the origin appears in
211+
/// multiple basic blocks and must participate in join operations. If false,
212+
/// the origin is block-local and can be discarded at block boundaries.
213+
llvm::BitVector PersistentOrigins;
117214
};
118215
} // namespace
119216

clang/unittests/Analysis/LifetimeSafetyTest.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,8 @@ TEST_F(LifetimeAnalysisTest, PointersInACycle) {
529529
MyObj* temp = p1;
530530
p1 = p2;
531531
p2 = p3;
532-
p3 = temp;
532+
p3 = temp;B
533+
POINT(in_loop);
533534
}
534535
POINT(after_loop);
535536
}
@@ -543,7 +544,11 @@ TEST_F(LifetimeAnalysisTest, PointersInACycle) {
543544
EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
544545
EXPECT_THAT(Origin("p2"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
545546
EXPECT_THAT(Origin("p3"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
546-
EXPECT_THAT(Origin("temp"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
547+
548+
EXPECT_THAT(Origin("temp"), HasLoansTo({"v1", "v2", "v3"}, "in_loop"));
549+
// 'temp' is a block-local origin and it's loans are not tracked outside the
550+
// block.
551+
EXPECT_THAT(Origin("temp"), HasLoansTo({}, "after_loop"));
547552
}
548553

549554
TEST_F(LifetimeAnalysisTest, PointersAndExpirationInACycle) {

0 commit comments

Comments
 (0)