[llvm] [LLVM][Transforms][Attributor] - Add batch reachability optimization to forallInterferingAccesses (PR #190078)
Pranav Bhandarkar via llvm-commits
llvm-commits at lists.llvm.org
Thu Apr 30 09:14:52 PDT 2026
================
@@ -1343,13 +1363,297 @@ struct AAPointerInfoImpl
return LeastDominatingWriteInst != Acc.getRemoteInst();
};
- // Run the user callback on all accesses we cannot skip and return if
- // that succeeded for all or not.
- for (auto &It : InterferingAccesses) {
- if ((!AllInSameNoSyncFn && !IsThreadLocalObj && !ExecDomainAA) ||
- !CanSkipAccess(*It.first, It.second)) {
- if (!UserCB(*It.first, It.second))
+ {
+ // Batch reachability optimization for CanSkipAccess.
+ //
+ // Without this optimization, each interfering access would trigger two
+ // independent AA::isPotentiallyReachable calls. Each call traverses the
+ // CFG from scratch. With N interfering accesses, this is 2 * N
+ // independent BFS traversals over the same function's CFG — redundant
+ // work.
+ //
+ // This optimization pre-computes two block-level reachability sets:
+ // ReachableFromI: all basic blocks reachable forward from I's block
+ // ReachableToI: all basic blocks that can reach I's block (backward)
+ //
+ // These are computed lazily (on first use) via a single BFS each,
+ // respecting the ExclusionSet (must-write barriers) and liveness
+ // (dead edges from AAIsDead). Then for each intra-function access,
+ // a cheap set lookup replaces the full isPotentiallyReachable call:
+ // - ReadChecked: if Acc's block is NOT in ReachableFromI, I can't
+ // reach Acc, so Acc's read can't observe I's write.
+ // - WriteChecked: if Acc's block is NOT in ReachableToI, Acc can't
+ // reach I, so Acc's write can't affect I's read.
+ //
+ // If the block-level check is inconclusive, we fall back to
+ // isPotentiallyReachable.
+ //
+ //
+
+ DenseSet<const BasicBlock *> ReachableFromI;
+ bool ReachableFromIComputed = false;
+
+ DenseSet<const BasicBlock *> ReachableToI;
+ bool ReachableToIComputed = false;
+
+ // Map each basic block to the ExclusionSet instructions it contains.
+ // Built once and shared across the BFS helpers and same-block checks
+ // in CanSkipAccessBatch, replacing per-use iteration over ExclusionSet.
+ DenseMap<const BasicBlock *, SmallVector<const Instruction *, 2>>
+ ExcludedBlockInsts;
+ for (const Instruction *ExclI : ExclusionSet)
+ ExcludedBlockInsts[ExclI->getParent()].push_back(ExclI);
+
+ // Lazily compute forward reachability from I's block.
+ // BFS over successor edges within Scope, skipping dead edges (AAIsDead)
+ // and not traversing past ExclusionSet blocks (must-write barriers).
+ // I's block is always traversed (its successors are always explored).
+ // Other blocks containing ExclusionSet instructions are added to the
+ // reachable set but their successors are NOT explored.
+ //
+ // Note: we do NOT block I's block even if it contains an ExclusionSet
+ // instruction after I. This matches isPotentiallyReachable /
+ // isReachableImpl semantics, which check SuccBB == ToBB before
+ // ExclusionBlocks and thus always consider direct successors as
+ // reachable.
+ auto EnsureReachableFromI = [&]() {
+ if (ReachableFromIComputed)
+ return;
+
+ const BasicBlock *FromBB = I.getParent();
+ SmallVector<const BasicBlock *, 16> Worklist;
+ Worklist.push_back(FromBB);
+ ReachableFromI.insert(FromBB);
+
+ const auto *LivenessAA = A.getAAFor<AAIsDead>(
+ QueryingAA, IRPosition::function(Scope), DepClassTy::OPTIONAL);
+
+ while (!Worklist.empty()) {
+ const BasicBlock *BB = Worklist.pop_back_val();
+
+ // Don't traverse past ExclusionSet blocks (must-write barriers),
+ // but always traverse I's own block.
+ if (BB != FromBB && ExcludedBlockInsts.count(BB))
+ continue;
+
+ for (const BasicBlock *SuccBB : successors(BB)) {
+ if (LivenessAA && LivenessAA->isEdgeDead(BB, SuccBB))
+ continue;
+
+ if (ReachableFromI.insert(SuccBB).second)
+ Worklist.push_back(SuccBB);
+ }
+ }
+ ReachableFromIComputed = true;
+ };
+
+ // Lazily compute backward reachability to I's block.
+ // BFS over predecessor edges within Scope, skipping dead edges and
+ // not traversing past ExclusionSet blocks. This is the mirror of
+ // EnsureReachableFromI: it answers "can Acc reach I?" rather than
+ // "can I reach Acc?".
+ //
+ // Lazily compute backward reachability to I's block.
+ // BFS over predecessor edges within Scope, skipping dead edges and
+ // not traversing past ExclusionSet blocks. This is the mirror of
+ // EnsureReachableFromI: it answers "can Acc reach I?" rather than
+ // "can I reach Acc?".
+ //
+ // As with EnsureReachableFromI, I's block is always traversed
+ // (its predecessors are always explored) to match isPotentiallyReachable
+ // semantics.
+ auto EnsureReachableToI = [&]() {
+ if (ReachableToIComputed)
+ return;
+
+ const BasicBlock *ToBB = I.getParent();
+ SmallVector<const BasicBlock *, 16> Worklist;
+ Worklist.push_back(ToBB);
+ ReachableToI.insert(ToBB);
+
+ const auto *LivenessAA = A.getAAFor<AAIsDead>(
+ QueryingAA, IRPosition::function(Scope), DepClassTy::OPTIONAL);
+
+ while (!Worklist.empty()) {
+ const BasicBlock *BB = Worklist.pop_back_val();
+
+ for (const BasicBlock *PredBB : predecessors(BB)) {
+ if (LivenessAA && LivenessAA->isEdgeDead(PredBB, BB))
+ continue;
+
+ if (!ReachableToI.insert(PredBB).second)
+ continue;
+
+ // Don't traverse past ExclusionSet blocks, but always
+ // traverse I's own block (ToBB).
+ bool Blocked = (PredBB != ToBB && ExcludedBlockInsts.count(PredBB));
+
+ if (!Blocked)
+ Worklist.push_back(PredBB);
+ }
+ }
+ ReachableToIComputed = true;
+ };
+
+ // Batch variant of CanSkipAccess for intra-function accesses.
+ //
+ // The batch BFS can safely be used as a negative filter (skip iPR when
+ // BFS says "not reachable") only when iPR agrees for all paths the BFS
+ // can't model. This requires:
+ // (a) norecurse: instructionCanReach can't find paths back to Scope
+ // (b) GoBackwardsCB returns false for Scope: iPR won't step back to
+ // callers
+ // Both hold for allocas in norecurse functions where
+ // GoBackwardsCB(*Scope) = IsLiveInCalleeCB(*Scope) = (AIFn != Scope) =
+ // false.
+ //
+ // When BFSSafe is true (alloca in norecurse fn), the BFS is used as a
+ // negative filter: if Acc's block is NOT in the reachable set,
+ // isPotentiallyReachable would also return false, so we can directly
+ // set Checked = true without calling isPotentiallyReachable.
+ //
+ // When BFSSafe is false, the BFS cannot be a negative filter because
+ // isPotentiallyreachable considers inter-procedural and cross-invocation
+ // paths that the BFS can't model. In that case, we skip the BFS entirely
+ // and only use the same-block optimization (which doesn't depend on the
+ // BFS).
+ bool BFSSafe = false;
+ if (isa<AllocaInst>(&getAssociatedValue()) && IsLiveInCalleeCB)
+ BFSSafe = true;
+
+ auto CanSkipAccessBatch = [&](const Access &Acc, bool Exact) {
+ if (SkipCB && SkipCB(Acc))
+ return true;
+ if (!CanIgnoreThreading(Acc)) {
return false;
+ }
+
+ bool ReadChecked = !FindInterferingReads;
+ bool WriteChecked = !FindInterferingWrites;
+
+ // Forward reachability: can I reach Acc?
+ if (!ReadChecked) {
+ if (BFSSafe) {
+ EnsureReachableFromI();
+ bool BlockReachable =
+ ReachableFromI.count(Acc.getRemoteInst()->getParent());
+
+ if (!BlockReachable) {
+ // BFS-safe negative filter: the BFS is a complete model of
+ // reachability for allocas in norecurse functions.
+ ReadChecked = true;
+ } else if (I.getParent() == Acc.getRemoteInst()->getParent() &&
+ I.comesBefore(Acc.getRemoteInst())) {
+ // Same-block optimization.
+ auto It = ExcludedBlockInsts.find(I.getParent());
+ if (It != ExcludedBlockInsts.end()) {
+ for (const Instruction *ExclI : It->second) {
+ if (I.comesBefore(ExclI) &&
+ ExclI->comesBefore(Acc.getRemoteInst())) {
+ ReadChecked = true;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ // Not BFS-safe: only apply same-block optimization.
+ if (I.getParent() == Acc.getRemoteInst()->getParent() &&
+ I.comesBefore(Acc.getRemoteInst())) {
+ auto It = ExcludedBlockInsts.find(I.getParent());
+ if (It != ExcludedBlockInsts.end()) {
+ for (const Instruction *ExclI : It->second) {
+ if (I.comesBefore(ExclI) &&
+ ExclI->comesBefore(Acc.getRemoteInst())) {
+ ReadChecked = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!ReadChecked) {
+ if (!AA::isPotentiallyReachable(A, I, *Acc.getRemoteInst(),
+ QueryingAA, &ExclusionSet,
+ IsLiveInCalleeCB)) {
+ ReadChecked = true;
+ }
+ }
+ }
+
+ // Backward reachability: can Acc reach I?
+ if (!WriteChecked) {
+ if (BFSSafe) {
+ EnsureReachableToI();
+ bool BlockReachable =
+ ReachableToI.count(Acc.getRemoteInst()->getParent());
+
+ if (!BlockReachable) {
+ WriteChecked = true;
+ } else if (I.getParent() == Acc.getRemoteInst()->getParent() &&
+ Acc.getRemoteInst()->comesBefore(&I)) {
+ auto It = ExcludedBlockInsts.find(I.getParent());
+ if (It != ExcludedBlockInsts.end()) {
+ for (const Instruction *ExclI : It->second) {
+ if (Acc.getRemoteInst()->comesBefore(ExclI) &&
+ ExclI->comesBefore(&I)) {
+ WriteChecked = true;
+ break;
+ }
+ }
+ }
+ }
+ } else {
+ if (I.getParent() == Acc.getRemoteInst()->getParent() &&
+ Acc.getRemoteInst()->comesBefore(&I)) {
+ auto It = ExcludedBlockInsts.find(I.getParent());
+ if (It != ExcludedBlockInsts.end()) {
+ for (const Instruction *ExclI : It->second) {
+ if (Acc.getRemoteInst()->comesBefore(ExclI) &&
+ ExclI->comesBefore(&I)) {
+ WriteChecked = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!WriteChecked) {
+ if (!AA::isPotentiallyReachable(A, *Acc.getRemoteInst(), I,
+ QueryingAA, &ExclusionSet,
+ IsLiveInCalleeCB)) {
+ WriteChecked = true;
+ }
+ }
+ }
+ if (ReadChecked && WriteChecked)
+ return true;
+
+ if (!DT || !UseDominanceReasoning)
+ return false;
+ if (!DominatingWrites.count(&Acc))
+ return false;
+ return LeastDominatingWriteInst != Acc.getRemoteInst();
+ };
+
+ // Process intra-function accesses.
+ for (auto &It : IntraFnAccesses) {
+ if ((!AllInSameNoSyncFn && !IsThreadLocalObj && !ExecDomainAA) ||
+ !CanSkipAccessBatch(*It.first, It.second)) {
+ if (!UserCB(*It.first, It.second))
+ return false;
+ }
+ }
+
+ // Process cross-function accesses.
+ for (auto &It : InterFnAccesses) {
+ if ((!AllInSameNoSyncFn && !IsThreadLocalObj && !ExecDomainAA) ||
+ !CanSkipAccess(*It.first, It.second)) {
+ if (!UserCB(*It.first, It.second))
+ return false;
+ }
----------------
bhandarkar-pranav wrote:
I see. I scanned the in-tree callers and didn't see one that obviously depends on insertion order, but the contract was previously "iteration order matches insertion order", and changing it relies on every present and future `UserCB` being order-insensitive. That's brittle.
I'll preserve the original order: keep one `SmallVector<std::pair<const Access *, bool>>` plus a parallel bit/enum indicating Intra vs. Inter, and dispatch to `CanSkipAccessBatch` / `CanSkipAccess` per-element in the original loop. The optimization (skipping the BFS for Inter accesses) is preserved; the externally visible iteration order is unchanged.
https://github.com/llvm/llvm-project/pull/190078
More information about the llvm-commits
mailing list