[llvm] [LLVM][Transforms][Attributor] - Optimize AAIsDeadFunction::isAssumedDead (PR #189467)
Pranav Bhandarkar via llvm-commits
llvm-commits at lists.llvm.org
Thu Apr 30 07:01:50 PDT 2026
https://github.com/bhandarkar-pranav updated https://github.com/llvm/llvm-project/pull/189467
>From aa0f9b7522a9f0c04a1856cb2c3f4a7cc90df800 Mon Sep 17 00:00:00 2001
From: Pranav Bhandarkar <pranav.bhandarkar at amd.com>
Date: Thu, 26 Feb 2026 11:46:52 -0600
Subject: [PATCH 1/4] [LLVM][Transforms][Attributor] - Optimize
AAIsDeadFunction::isAssumedDead
This patch optimizes `AAIsDeadFunction::isAssumedDead` to address a performance
bottleneck identified during the linking of a representative workload.
**Optimized `AAIsDeadFunction::isAssumedDead`**:
- Replaced the $O(N^2)$ backward linear scan for instruction liveness with a
cached "First Dead Instruction" approach.
- Added `FirstDeadInstCache` to `AAIsDeadFunction`.
- On the first query, the block is scanned once ($O(N)$) to find the first
dead instruction. Subsequent queries use this cache and `comesBefore` check
(or simple identity check), reducing the complexity significantly.
- Added proper cache invalidation in `updateImpl` and `manifest`.
Assisted-by: Cursor.
---
.../Transforms/IPO/AttributorAttributes.cpp | 55 ++++++++++++++++---
1 file changed, 48 insertions(+), 7 deletions(-)
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 95c0531c2183b..f6453037c3752 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -4556,6 +4556,9 @@ struct AAIsDeadFunction : public AAIsDead {
HasChanged = ChangeStatus::CHANGED;
}
+ if (HasChanged == ChangeStatus::CHANGED)
+ FirstDeadInstCache.clear();
+
return HasChanged;
}
@@ -4606,14 +4609,41 @@ struct AAIsDeadFunction : public AAIsDead {
if (!AssumedLiveBlocks.count(I->getParent()))
return true;
- // If it is not after a liveness barrier it is live.
- const Instruction *PrevI = I->getPrevNode();
- while (PrevI) {
- if (KnownDeadEnds.count(PrevI) || ToBeExploredFrom.count(PrevI))
- return true;
- PrevI = PrevI->getPrevNode();
+ // We cache the *first* dead instruction in the block.
+ // If such an instruction exists and precedes I, then I is dead.
+ // Previously, we used to a do a backwards linear scan from I to
+ // the beginning of the block, checking KnownDeadEnds and ToBeExploredFrom
+ // at each step. By caching we trade complexity for storage.
+
+ const BasicBlock *BB = I->getParent();
+ auto It = FirstDeadInstCache.find(BB);
+ if (It == FirstDeadInstCache.end()) {
+ // Cache miss. Scan the block forward to find the first dead end.
+ const Instruction *FirstDead = nullptr;
+ for (const Instruction &Inst : *BB) {
+ if (KnownDeadEnds.count(&Inst) || ToBeExploredFrom.count(&Inst)) {
+ FirstDead = &Inst;
+ break;
+ }
+ }
+ It = FirstDeadInstCache.insert({BB, FirstDead}).first;
}
- return false;
+
+ const Instruction *FirstDead = It->second;
+
+ // If no dead end in the block, I is not dead (via this mechanism).
+ if (!FirstDead)
+ return false;
+
+ // If I is the first dead end, it is not dead *after* a barrier (it IS the
+ // barrier).
+ if (FirstDead == I)
+ return false;
+
+ // If FirstDead comes before I, then I is dead.
+ // Note: comesBefore is O(N), but it avoids the hash lookups of the original
+ // loop. Also, we only scan from FirstDead to I, not from I to start.
+ return FirstDead->comesBefore(I);
}
/// See AAIsDead::isKnownDead(Instruction *I).
@@ -4651,6 +4681,12 @@ struct AAIsDeadFunction : public AAIsDead {
/// Collection of all assumed live BasicBlocks.
DenseSet<const BasicBlock *> AssumedLiveBlocks;
+
+ /// Cache to store the first "dead end" instruction for each basic block.
+ /// A "dead end" is an instruction in KnownDeadEnds or ToBeExploredFrom.
+ /// If the mapped value is nullptr, the block has no dead ends.
+ /// If it is non-null, it points to the first such instruction in the block.
+ mutable DenseMap<const BasicBlock *, const Instruction *> FirstDeadInstCache;
};
static bool
@@ -4885,6 +4921,11 @@ ChangeStatus AAIsDeadFunction::updateImpl(Attributor &A) {
ToBeExploredFrom = std::move(NewToBeExploredFrom);
}
+ // If the state changed (KnownDeadEnds or ToBeExploredFrom), the cache is
+ // invalid.
+ if (Change == ChangeStatus::CHANGED)
+ FirstDeadInstCache.clear();
+
// If we know everything is live there is no need to query for liveness.
// Instead, indicating a pessimistic fixpoint will cause the state to be
// "invalid" and all queries to be answered conservatively without lookups.
>From 2fd97ef871b9d8fbcbaf3cd537cc8352ee1bece4 Mon Sep 17 00:00:00 2001
From: Pranav Bhandarkar <pranav.bhandarkar at amd.com>
Date: Wed, 15 Apr 2026 22:32:00 -0500
Subject: [PATCH 2/4] [Attributor] Invalidate FirsftDeadInstCache per-block
when KnownDeadEnds grows
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When KnownDeadEnds.insert(I) adds a new dead end during the
updateImpl worklist loop, the FirstDeadInstCache entry for
I->getParent() may become stale — the cached first-dead instruction
might come after the newly added one, or the block may have had no
cached entry at all.
If identifyAliveSuccessors triggers a call chain (via getAAFor)
that queries isAssumedDead on this instance before the bulk
cache clear at the end of updateImpl, it would use the stale
entry. The staleness is conservative (isAssumedDead returns
false instead of true, never the reverse), so there is no
miscompile risk, but it is cleaner to keep the cache consistent.
Invalidate just the affected block's entry on each insertion
rather than moving the bulk clear into the loop.
---
llvm/lib/Transforms/IPO/AttributorAttributes.cpp | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index f6453037c3752..79911eefdb095 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -4611,7 +4611,7 @@ struct AAIsDeadFunction : public AAIsDead {
// We cache the *first* dead instruction in the block.
// If such an instruction exists and precedes I, then I is dead.
- // Previously, we used to a do a backwards linear scan from I to
+ // Previously, we used to do a backwards linear scan from I to
// the beginning of the block, checking KnownDeadEnds and ToBeExploredFrom
// at each step. By caching we trade complexity for storage.
@@ -4888,8 +4888,16 @@ ChangeStatus AAIsDeadFunction::updateImpl(Attributor &A) {
} else if (AliveSuccessors.empty() ||
(I->isTerminator() &&
AliveSuccessors.size() < I->getNumSuccessors())) {
- if (KnownDeadEnds.insert(I))
+ if (KnownDeadEnds.insert(I)) {
Change = ChangeStatus::CHANGED;
+ // Invalidate the cached first-dead-instruction for this block,
+ // since the newly added dead end may precede the previously
+ // cached entry (or the block may have had no cached dead end).
+ // A stale cache could be observed if identifyAliveSuccessors
+ // triggers a call chain (via getAAFor) that queries
+ // isAssumedDead on this instance before the bulk clear below.
+ FirstDeadInstCache.erase(I->getParent());
+ }
}
LLVM_DEBUG(dbgs() << "[AAIsDead] #AliveSuccessors: "
>From a0a571c924679a9a1c2e06a2c6284c632d95473d Mon Sep 17 00:00:00 2001
From: Pranav Bhandarkar <pranav.bhandarkar at amd.com>
Date: Mon, 20 Apr 2026 16:06:18 -0500
Subject: [PATCH 3/4] Update misleading comment in
AAIsDeadFunction::isAssumedDead(const Instruction *I)
---
llvm/lib/Transforms/IPO/AttributorAttributes.cpp | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 79911eefdb095..90099e879720b 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -4609,12 +4609,14 @@ struct AAIsDeadFunction : public AAIsDead {
if (!AssumedLiveBlocks.count(I->getParent()))
return true;
- // We cache the *first* dead instruction in the block.
- // If such an instruction exists and precedes I, then I is dead.
+ // We cache the *first* liveness barrier in the block. A liveness barrier
+ // is an instruction in KnownDeadEnds or ToBeExploredFrom — these are
+ // always terminators or calls that are known or assumed to not transfer
+ // control to their successor. If such an instruction exists and precedes
+ // I in the block, then I is unreachable and therefore dead.
// Previously, we used to do a backwards linear scan from I to
// the beginning of the block, checking KnownDeadEnds and ToBeExploredFrom
// at each step. By caching we trade complexity for storage.
-
const BasicBlock *BB = I->getParent();
auto It = FirstDeadInstCache.find(BB);
if (It == FirstDeadInstCache.end()) {
>From 9eb93525e18d409809ff08b4cdec29b316470554 Mon Sep 17 00:00:00 2001
From: Pranav Bhandarkar <pranav.bhandarkar at amd.com>
Date: Thu, 30 Apr 2026 09:00:33 -0500
Subject: [PATCH 4/4] [Attributor] Rename FirstDeadInstCache to
FirstBarrierInstCache (NFC)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The old names (FirstDeadInstCache, FirstDead) were misleading — they
suggested we track dead instructions, but we actually track the first
liveness barrier (an instruction in KnownDeadEnds or ToBeExploredFrom
that does not transfer control to its successor). Rename to
FirstBarrierInstCache / FirstBarrierInst and update comments to use
"liveness barrier" terminology consistently.
Made-with: Cursor
---
.../Transforms/IPO/AttributorAttributes.cpp | 52 ++++++++++---------
1 file changed, 27 insertions(+), 25 deletions(-)
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 90099e879720b..a7315a57d13a9 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -4557,7 +4557,7 @@ struct AAIsDeadFunction : public AAIsDead {
}
if (HasChanged == ChangeStatus::CHANGED)
- FirstDeadInstCache.clear();
+ FirstBarrierInstCache.clear();
return HasChanged;
}
@@ -4618,34 +4618,35 @@ struct AAIsDeadFunction : public AAIsDead {
// the beginning of the block, checking KnownDeadEnds and ToBeExploredFrom
// at each step. By caching we trade complexity for storage.
const BasicBlock *BB = I->getParent();
- auto It = FirstDeadInstCache.find(BB);
- if (It == FirstDeadInstCache.end()) {
- // Cache miss. Scan the block forward to find the first dead end.
- const Instruction *FirstDead = nullptr;
+ auto It = FirstBarrierInstCache.find(BB);
+ if (It == FirstBarrierInstCache.end()) {
+ // Cache miss. Scan the block forward to find the first liveness barrier.
+ const Instruction *FirstBarrierInst = nullptr;
for (const Instruction &Inst : *BB) {
if (KnownDeadEnds.count(&Inst) || ToBeExploredFrom.count(&Inst)) {
- FirstDead = &Inst;
+ FirstBarrierInst = &Inst;
break;
}
}
- It = FirstDeadInstCache.insert({BB, FirstDead}).first;
+ It = FirstBarrierInstCache.insert({BB, FirstBarrierInst}).first;
}
- const Instruction *FirstDead = It->second;
+ const Instruction *FirstBarrierInst = It->second;
- // If no dead end in the block, I is not dead (via this mechanism).
- if (!FirstDead)
+ // If no liveness barrier in the block, I is not dead (via this mechanism).
+ if (!FirstBarrierInst)
return false;
- // If I is the first dead end, it is not dead *after* a barrier (it IS the
- // barrier).
- if (FirstDead == I)
+ // If I is the first liveness barrier, it is not dead *after* a barrier
+ // (it IS the barrier).
+ if (FirstBarrierInst == I)
return false;
- // If FirstDead comes before I, then I is dead.
+ // If FirstBarrierInst comes before I, then I is dead.
// Note: comesBefore is O(N), but it avoids the hash lookups of the original
- // loop. Also, we only scan from FirstDead to I, not from I to start.
- return FirstDead->comesBefore(I);
+ // loop. Also, we only scan from FirstBarrierInst to I, not from I to
+ // start.
+ return FirstBarrierInst->comesBefore(I);
}
/// See AAIsDead::isKnownDead(Instruction *I).
@@ -4684,11 +4685,12 @@ struct AAIsDeadFunction : public AAIsDead {
/// Collection of all assumed live BasicBlocks.
DenseSet<const BasicBlock *> AssumedLiveBlocks;
- /// Cache to store the first "dead end" instruction for each basic block.
- /// A "dead end" is an instruction in KnownDeadEnds or ToBeExploredFrom.
- /// If the mapped value is nullptr, the block has no dead ends.
- /// If it is non-null, it points to the first such instruction in the block.
- mutable DenseMap<const BasicBlock *, const Instruction *> FirstDeadInstCache;
+ /// Cache mapping each basic block to its first liveness barrier instruction.
+ /// A liveness barrier is an instruction in KnownDeadEnds or ToBeExploredFrom
+ /// — a terminator or call that is known/assumed to not transfer control to
+ /// its successor. If the mapped value is nullptr, the block has no barrier.
+ /// If non-null, it points to the first such instruction in the block.
+ mutable DenseMap<const BasicBlock *, const Instruction *> FirstBarrierInstCache;
};
static bool
@@ -4892,13 +4894,13 @@ ChangeStatus AAIsDeadFunction::updateImpl(Attributor &A) {
AliveSuccessors.size() < I->getNumSuccessors())) {
if (KnownDeadEnds.insert(I)) {
Change = ChangeStatus::CHANGED;
- // Invalidate the cached first-dead-instruction for this block,
+ // Invalidate the cached first liveness barrier for this block,
// since the newly added dead end may precede the previously
- // cached entry (or the block may have had no cached dead end).
+ // cached barrier (or the block may have had no cached barrier).
// A stale cache could be observed if identifyAliveSuccessors
// triggers a call chain (via getAAFor) that queries
// isAssumedDead on this instance before the bulk clear below.
- FirstDeadInstCache.erase(I->getParent());
+ FirstBarrierInstCache.erase(I->getParent());
}
}
@@ -4934,7 +4936,7 @@ ChangeStatus AAIsDeadFunction::updateImpl(Attributor &A) {
// If the state changed (KnownDeadEnds or ToBeExploredFrom), the cache is
// invalid.
if (Change == ChangeStatus::CHANGED)
- FirstDeadInstCache.clear();
+ FirstBarrierInstCache.clear();
// If we know everything is live there is no need to query for liveness.
// Instead, indicating a pessimistic fixpoint will cause the state to be
More information about the llvm-commits
mailing list