[flang-commits] [flang] ef20856 - [fir][aa] Add opt-in cache for use by fir `AliasAnalysis` clients (#221973)
via flang-commits
flang-commits at lists.llvm.org
Wed Sep 9 04:28:28 PDT 2026
Author: Kareem Ergawy
Date: 2026-09-09T13:28:23+02:00
New Revision: ef208564acbfc9e8fef0d6c436bac1681f1a6778
URL: https://github.com/llvm/llvm-project/commit/ef208564acbfc9e8fef0d6c436bac1681f1a6778
DIFF: https://github.com/llvm/llvm-project/commit/ef208564acbfc9e8fef0d6c436bac1681f1a6778.diff
LOG: [fir][aa] Add opt-in cache for use by fir `AliasAnalysis` clients (#221973)
Adds `AliasAnalysisRecursiveEffectsCache`, an opt-in cache that memoizes
per-operation read/write summaries so that `getModRef` on an operation
with `HasRecursiveMemoryEffects` does not re-walk its nested regions on
every query.
This is independent of the `getSource()` memoization added separately:
that one is keyed on (value, flags) and answers "what memory does this
value refer to", while this one is keyed on the operation and answers
"what does this operation and everything nested in it read and write".
LICM enables both, since it only hoists pure-read ops and so invalidates
neither.
Added:
flang/unittests/Optimizer/AliasAnalysisRecursiveEffectsCacheTest.cpp
Modified:
flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
flang/unittests/Optimizer/CMakeLists.txt
Removed:
################################################################################
diff --git a/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h b/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
index 98d92e683a2a7..d9429e96e208d 100644
--- a/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
+++ b/flang/include/flang/Optimizer/Analysis/AliasAnalysis.h
@@ -25,10 +25,25 @@
namespace fir {
+class AliasAnalysisRecursiveEffectsCache;
+
//===----------------------------------------------------------------------===//
// AliasAnalysis
//===----------------------------------------------------------------------===//
struct AliasAnalysis {
+ AliasAnalysis() = default;
+
+ /// Construct an alias analysis bound to `cache`.
+ explicit AliasAnalysis(AliasAnalysisRecursiveEffectsCache &cache);
+
+ AliasAnalysis(AliasAnalysis &&other) noexcept;
+
+ ~AliasAnalysis();
+
+ AliasAnalysis(const AliasAnalysis &) = delete;
+ AliasAnalysis &operator=(const AliasAnalysis &) = delete;
+ AliasAnalysis &operator=(AliasAnalysis &&) = delete;
+
// Structures to describe the memory source of a value.
/// Kind of the memory source referenced by a value.
@@ -405,6 +420,8 @@ struct AliasAnalysis {
bool functionHasMultipleScopes(mlir::Value v);
private:
+ friend class AliasAnalysisRecursiveEffectsCache;
+
/// Compute the memory source of a value. This is the uncached
/// implementation of getSource(); getSource() is a thin wrapper that
/// memoizes the result when source caching is enabled.
@@ -506,6 +523,78 @@ struct AliasAnalysis {
/// on each getSource() cache hit / miss.
std::size_t sourceCacheHits = 0;
std::size_t sourceCacheMisses = 0;
+
+ /// Optional opt-in cache for getModRef on ops with HasRecursiveMemoryEffects.
+ AliasAnalysisRecursiveEffectsCache *cache = nullptr;
+};
+
+/// Opt-in cache that amortizes the cost of repeated AliasAnalysis::getModRef
+/// queries against an op with HasRecursiveMemoryEffects (e.g. a loop).
+class AliasAnalysisRecursiveEffectsCache {
+public:
+ AliasAnalysisRecursiveEffectsCache() = default;
+ ~AliasAnalysisRecursiveEffectsCache() {
+ if (aa)
+ aa->cache = nullptr;
+ }
+
+ AliasAnalysisRecursiveEffectsCache(
+ const AliasAnalysisRecursiveEffectsCache &) = delete;
+ AliasAnalysisRecursiveEffectsCache &
+ operator=(const AliasAnalysisRecursiveEffectsCache &) = delete;
+ AliasAnalysisRecursiveEffectsCache(AliasAnalysisRecursiveEffectsCache &&) =
+ delete;
+ AliasAnalysisRecursiveEffectsCache &
+ operator=(AliasAnalysisRecursiveEffectsCache &&) = delete;
+
+ /// Drop all cached summaries. Call this when the IR inside previously
+ /// summarized ops has been mutated in a way the cache cannot tolerate.
+ void clear() { summaries.clear(); }
+
+ /// Testing only: number of op summaries currently held.
+ std::size_t getSummaryCacheSizeForTesting() const { return summaries.size(); }
+
+ /// Testing only: cumulative summary lookups that were served from an
+ /// existing entry (hits) or required buildSummary() (misses).
+ std::size_t getSummaryCacheHitsForTesting() const { return summaryHits; }
+ std::size_t getSummaryCacheMissesForTesting() const { return summaryMisses; }
+
+private:
+ friend struct AliasAnalysis;
+
+ struct CallInfo {
+ mlir::Operation *op;
+ /// If false, AliasAnalysis::getCallModRef would unconditionally return
+ /// ModAndRef on this call (e.g. runtime call / external procedure), so
+ /// per-query analysis skips it and goes straight to interface-effect
+ /// fall-through.
+ bool isFortranUserProcedure;
+ };
+
+ struct Summary {
+ bool hasUnknownWrite = false;
+ bool hasUnknownRead = false;
+ llvm::SmallVector<mlir::Value, 16> writeLocations;
+ llvm::SmallVector<mlir::Value, 16> readLocations;
+ llvm::SmallVector<CallInfo, 4> calls;
+ };
+
+ /// Populate `out` with the effects of `op` itself and, if op has
+ /// HasRecursiveMemoryEffects, recursively those of every op nested in its
+ /// regions.
+ void buildSummary(mlir::Operation *op, Summary &out);
+ void buildSummary(mlir::Region ®ion, Summary &out);
+
+ mlir::ModRefResult getModRefFromSummary(mlir::Operation *op,
+ mlir::Value location);
+
+ /// Back-pointer to the AliasAnalysis this cache is linked with.
+ AliasAnalysis *aa = nullptr;
+ llvm::DenseMap<mlir::Operation *, Summary> summaries;
+
+ /// Testing-only counters (see getSummaryCacheHitsForTesting()).
+ std::size_t summaryHits = 0;
+ std::size_t summaryMisses = 0;
};
inline bool operator==(const AliasAnalysis::Source::SourceOrigin &lhs,
diff --git a/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp b/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
index 32429d554b7a1..5d60b373b0819 100644
--- a/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
+++ b/flang/lib/Optimizer/Analysis/AliasAnalysis.cpp
@@ -1095,9 +1095,237 @@ ModRefResult AliasAnalysis::getCallModRef(Operation *op, Value var) {
return ModRefResult::getNoModRef();
}
+AliasAnalysis::AliasAnalysis(AliasAnalysisRecursiveEffectsCache &cacheRef)
+ : cache(&cacheRef) {
+ cacheRef.aa = this;
+}
+
+AliasAnalysis::AliasAnalysis(AliasAnalysis &&other) noexcept
+ : symTabMap(std::move(other.symTabMap)),
+ domInfoCache(std::move(other.domInfoCache)),
+ sortedScopeCache(std::move(other.sortedScopeCache)),
+ multiScopeCache(std::move(other.multiScopeCache)),
+ getSourceCache(std::move(other.getSourceCache)),
+ sourceCacheEnabled(other.sourceCacheEnabled),
+ sourceCacheHits(other.sourceCacheHits),
+ sourceCacheMisses(other.sourceCacheMisses), cache(other.cache) {
+ other.cache = nullptr;
+ if (cache)
+ cache->aa = this;
+}
+
+AliasAnalysis::~AliasAnalysis() {
+ if (cache)
+ cache->aa = nullptr;
+}
+
+//===----------------------------------------------------------------------===//
+// AliasAnalysisRecursiveEffectsCache
+//===----------------------------------------------------------------------===//
+
+void AliasAnalysisRecursiveEffectsCache::buildSummary(mlir::Region ®ion,
+ Summary &out) {
+ for (mlir::Operation &op : region.getOps())
+ buildSummary(&op, out);
+}
+
+void AliasAnalysisRecursiveEffectsCache::buildSummary(mlir::Operation *op,
+ Summary &out) {
+ // fir.call: defer the entire analysis to per-query getCallModRef. Recording
+ // its generic interface effects here on top would over-pessimize: the
+ // uncached path only falls through to interface analysis when
+ // getCallModRef itself returns ModAndRef.
+ if (llvm::isa<fir::CallOp>(op)) {
+ CallInfo ci;
+ ci.op = op;
+ ci.isFortranUserProcedure = aa->isCallToFortranUserProcedure(op);
+ out.calls.push_back(ci);
+ return;
+ }
+
+ bool isRecursive = op->hasTrait<mlir::OpTrait::HasRecursiveMemoryEffects>();
+ if (isRecursive) {
+ for (mlir::Region &r : op->getRegions())
+ buildSummary(r, out);
+ }
+
+ auto iface = dyn_cast<MemoryEffectOpInterface>(op);
+
+ if (!iface) {
+ // No effect interface and not handled by the recursive branch: must
+ // conservatively assume both Mod and Ref (this mirrors the uncached
+ // AliasAnalysis::getModRef which returns ModAndRef in this case).
+ if (!isRecursive) {
+ out.hasUnknownWrite = true;
+ out.hasUnknownRead = true;
+ }
+ return;
+ }
+
+ SmallVector<MemoryEffects::EffectInstance> effects;
+ iface.getEffects(effects);
+
+ for (const MemoryEffects::EffectInstance &effect : effects) {
+ if (isa<MemoryEffects::Allocate, MemoryEffects::Free>(effect.getEffect()))
+ continue;
+
+ mlir::SideEffects::Resource *resource = effect.getResource();
+
+ if (!resource->isAddressable())
+ continue;
+
+ bool isRead = isa<MemoryEffects::Read>(effect.getEffect());
+ bool isWrite = isa<MemoryEffects::Write>(effect.getEffect());
+ mlir::Value v = effect.getValue();
+
+ if (!v) {
+ if (isRead)
+ out.hasUnknownRead = true;
+ if (isWrite)
+ out.hasUnknownWrite = true;
+ continue;
+ }
+
+ if (isRead)
+ out.readLocations.push_back(v);
+
+ if (isWrite)
+ out.writeLocations.push_back(v);
+ }
+}
+
+ModRefResult
+AliasAnalysisRecursiveEffectsCache::getModRefFromSummary(mlir::Operation *op,
+ mlir::Value location) {
+ assert(aa &&
+ "cache used without a linked fir::AliasAnalysis; this should only "
+ "be invoked from AliasAnalysis::getModRef when the back-pointer is "
+ "set");
+ auto it = summaries.find(op);
+
+ if (it == summaries.end()) {
+ ++summaryMisses;
+ Summary s;
+ buildSummary(op, s);
+ it = summaries.try_emplace(op, std::move(s)).first;
+ } else {
+ ++summaryHits;
+ }
+
+ const Summary &s = it->second;
+ bool mod = s.hasUnknownWrite;
+ bool ref = s.hasUnknownRead;
+
+ if (!mod) {
+ for (mlir::Value v : s.writeLocations) {
+ if (!aa->alias(v, location).isNo()) {
+ mod = true;
+ break;
+ }
+ }
+ }
+
+ if (!ref) {
+ for (mlir::Value v : s.readLocations) {
+ if (!aa->alias(v, location).isNo()) {
+ ref = true;
+ break;
+ }
+ }
+ }
+
+ if (mod && ref)
+ return ModRefResult::getModAndRef();
+
+ for (const CallInfo &ci : s.calls) {
+ mlir::Operation *call = ci.op;
+
+ if (ci.isFortranUserProcedure) {
+ ModRefResult cr = aa->getCallModRef(call, location);
+
+ if (cr != ModRefResult::getModAndRef()) {
+ if (cr.isMod())
+ mod = true;
+
+ if (cr.isRef())
+ ref = true;
+
+ if (mod && ref)
+ break;
+
+ continue;
+ }
+ // Fall through to interface analysis below.
+ }
+ // Either getCallModRef gave conservative ModAndRef, or the callee is
+ // not a Fortran user procedure (in which case getCallModRef would
+ // unconditionally return ModAndRef). Mirror the uncached fall-through
+ // to MemoryEffectOpInterface for additional precision.
+ auto iface = dyn_cast<MemoryEffectOpInterface>(call);
+
+ if (!iface) {
+ mod = true;
+ ref = true;
+ break;
+ }
+
+ SmallVector<MemoryEffects::EffectInstance> callEffects;
+ iface.getEffects(callEffects);
+
+ for (const MemoryEffects::EffectInstance &effect : callEffects) {
+ if (isa<MemoryEffects::Allocate, MemoryEffects::Free>(effect.getEffect()))
+ continue;
+
+ mlir::SideEffects::Resource *resource = effect.getResource();
+
+ if (!resource->isAddressable())
+ continue;
+
+ AliasResult ar = AliasResult::MayAlias;
+
+ if (mlir::Value v = effect.getValue())
+ ar = aa->alias(v, location);
+
+ if (ar.isNo())
+ continue;
+
+ if (isa<MemoryEffects::Read>(effect.getEffect()))
+ ref = true;
+
+ if (isa<MemoryEffects::Write>(effect.getEffect()))
+ mod = true;
+
+ if (mod && ref)
+ break;
+ }
+
+ if (mod && ref)
+ break;
+ }
+
+ if (mod && ref)
+ return ModRefResult::getModAndRef();
+
+ if (mod)
+ return ModRefResult::getMod();
+
+ if (ref)
+ return ModRefResult::getRef();
+
+ return ModRefResult::getNoModRef();
+}
+
/// This is mostly inspired by MLIR::LocalAliasAnalysis, except that
/// fir.call's are handled in a special way.
ModRefResult AliasAnalysis::getModRef(Operation *op, Value location) {
+ // If this AliasAnalysis is linked with a cache, route ops with
+ // HasRecursiveMemoryEffects through it. Non-recursive ops fall through to
+ // the inline path below; the cache eventually delegates back to
+ // alias()/getCallModRef() on this instance, which never re-enter this
+ // routing check, so there is no risk of infinite recursion.
+ if (cache && op->hasTrait<mlir::OpTrait::HasRecursiveMemoryEffects>())
+ return cache->getModRefFromSummary(op, location);
+
if (auto call = llvm::dyn_cast<fir::CallOp>(op)) {
ModRefResult result = getCallModRef(call, location);
if (result != ModRefResult::getModAndRef())
diff --git a/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp b/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
index de944ef07fbac..97958003ddfd2 100644
--- a/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
+++ b/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
@@ -287,14 +287,34 @@ void LoopInvariantCodeMotion::runOnOperation() {
LDBG() << "Enter [HL]FIR LoopInvariantCodeMotion()";
+ // Build a recursive-effects cache scoped to this pass run and link it to
+ // a fir::AliasAnalysis that will live inside the mlir::AliasAnalysis
+ // aggregator. Every query against that AliasAnalysis (direct, or via the
+ // aggregator) now routes recursive-effect ops through the cache. The
+ // cache's destructor nulls the back-pointer on the registered
+ // AliasAnalysis when LICM exits, so the aggregator never dereferences a
+ // dead cache.
+ //
+ // LICM only hoists pure-read ops out of loops; writes are never moved,
+ // ops are never erased, and SSA values are not RAUW'd. That matches the
+ // cache's safety invariant for the whole pass run on this function.
+ fir::AliasAnalysisRecursiveEffectsCache cachedAA;
auto &aliasAnalysis = getAnalysis<AliasAnalysis>();
- // Enable getSource() memoization on the FIR AliasAnalysis for the duration
- // of this pass. This is a frozen-snapshot cache with no automatic
- // invalidation, but it is sound here because LICM only moves operations, so
- // getSource()'s inputs are unchanged across the hoists. The cache lives no
- // longer than this analysis instance, which the pass manager drops when the
- // analysis is invalidated after the pass.
- fir::AliasAnalysis firAliasAnalysis;
+ // Two independent, complementary caches are enabled for this pass:
+ //
+ // * the recursive-effects cache (`cachedAA`), which memoizes per-operation
+ // read/write summaries so mod-ref queries do not re-walk the regions of
+ // ops with HasRecursiveMemoryEffects, and
+ // * getSource() memoization, which memoizes source classification keyed on
+ // (value, flags).
+ //
+ // Both are frozen-snapshot caches with no automatic invalidation, and both
+ // are sound here for the same reason: LICM only hoists pure-read ops, never
+ // moves writes, erases ops, or RAUWs values, so neither the effects of an
+ // operation nor the source of a value changes across the hoists. They live
+ // no longer than this analysis instance, which the pass manager drops when
+ // the analysis is invalidated after the pass.
+ fir::AliasAnalysis firAliasAnalysis{cachedAA};
firAliasAnalysis.enableSourceCache();
aliasAnalysis.addAnalysisImplementation(std::move(firAliasAnalysis));
diff --git a/flang/unittests/Optimizer/AliasAnalysisRecursiveEffectsCacheTest.cpp b/flang/unittests/Optimizer/AliasAnalysisRecursiveEffectsCacheTest.cpp
new file mode 100644
index 0000000000000..b559790e85d2e
--- /dev/null
+++ b/flang/unittests/Optimizer/AliasAnalysisRecursiveEffectsCacheTest.cpp
@@ -0,0 +1,241 @@
+//===- AliasAnalysisRecursiveEffectsCacheTest.cpp -------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Unit tests for AliasAnalysisRecursiveEffectsCache, the opt-in cache that
+// amortizes AliasAnalysis::getModRef queries against ops carrying
+// HasRecursiveMemoryEffects (e.g. fir.do_loop). The cache is only consulted
+// when an AliasAnalysis is constructed from one; it summarizes an op's nested
+// effects once and then answers subsequent queries from that summary.
+//
+// The cache is a frozen snapshot with no automatic invalidation: a client
+// enables it only across a region in which it does not mutate the summarized
+// ops' bodies, and calls clear() otherwise.
+//
+//===----------------------------------------------------------------------===//
+
+#include "gtest/gtest.h"
+#include "mlir/Dialect/Arith/IR/Arith.h"
+#include "flang/Optimizer/Analysis/AliasAnalysis.h"
+#include "flang/Optimizer/Dialect/FIROps.h"
+#include "flang/Optimizer/Support/InitFIR.h"
+
+struct AliasAnalysisRecursiveEffectsCacheTest : public testing::Test {
+public:
+ void SetUp() override {
+ fir::support::loadDialects(context);
+ builder = std::make_unique<mlir::OpBuilder>(&context);
+ mlir::Location loc = builder->getUnknownLoc();
+
+ moduleOp = mlir::ModuleOp::create(*builder, loc);
+ builder->setInsertionPointToStart(moduleOp->getBody());
+ mlir::func::FuncOp func = mlir::func::FuncOp::create(*builder, loc,
+ "alias_analysis_recursive_cache_tests",
+ builder->getFunctionType({}, {}));
+ builder->setInsertionPointToStart(func.addEntryBlock());
+ }
+
+ mlir::Location getLoc() { return builder->getUnknownLoc(); }
+
+ mlir::Value createAlloca() {
+ return fir::AllocaOp::create(
+ *builder, getLoc(), mlir::Float32Type::get(&context));
+ }
+
+ mlir::Value createIndex(std::int64_t v) {
+ return mlir::arith::ConstantIndexOp::create(*builder, getLoc(), v);
+ }
+
+ /// Build `fir.do_loop %i = 0 to 10 step 1 { fir.store %cst to <dest> }` and
+ /// return the loop op. fir.do_loop carries HasRecursiveMemoryEffects, so the
+ /// cache summarizes the nested store rather than re-walking the body on
+ /// every query.
+ fir::DoLoopOp createLoopStoringTo(mlir::Value dest) {
+ mlir::Value lb = createIndex(0);
+ mlir::Value ub = createIndex(10);
+ mlir::Value step = createIndex(1);
+ auto loop = fir::DoLoopOp::create(*builder, getLoc(), lb, ub, step);
+
+ mlir::OpBuilder::InsertionGuard guard(*builder);
+ builder->setInsertionPointToStart(loop.getBody());
+ mlir::Value cst = mlir::arith::ConstantOp::create(
+ *builder, getLoc(), builder->getF32FloatAttr(0.0f));
+ fir::StoreOp::create(*builder, getLoc(), cst, dest);
+ return loop;
+ }
+
+ mlir::MLIRContext context;
+ std::unique_ptr<mlir::OpBuilder> builder;
+ mlir::OwningOpRef<mlir::ModuleOp> moduleOp;
+};
+
+// An AliasAnalysis built without a cache never routes through one: the cache
+// object stays empty and its counters stay at zero.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, UnusedWhenNotLinked) {
+ mlir::Value a = createAlloca();
+ fir::DoLoopOp loop = createLoopStoringTo(a);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis aa; // deliberately not constructed from `cache`
+
+ (void)aa.getModRef(loop, a);
+ (void)aa.getModRef(loop, a);
+
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 0u);
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 0u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 0u);
+}
+
+// The first query against a recursive op is a miss that builds and stores one
+// summary; repeating it is a hit served from that summary, with no new entry.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, MissThenHit) {
+ mlir::Value a = createAlloca();
+ fir::DoLoopOp loop = createLoopStoringTo(a);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis aa{cache};
+
+ (void)aa.getModRef(loop, a);
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 0u);
+
+ (void)aa.getModRef(loop, a);
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 1u);
+
+ // The summary is keyed on the op alone, not on the queried location, so a
+ //
diff erent location against the same loop is also a hit.
+ mlir::Value b = createAlloca();
+ (void)aa.getModRef(loop, b);
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 2u);
+}
+
+// Distinct recursive ops are summarized independently.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, DistinctOpsGetDistinctEntries) {
+ mlir::Value a = createAlloca();
+ mlir::Value b = createAlloca();
+ fir::DoLoopOp loopA = createLoopStoringTo(a);
+ fir::DoLoopOp loopB = createLoopStoringTo(b);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis aa{cache};
+
+ (void)aa.getModRef(loopA, a);
+ (void)aa.getModRef(loopB, b);
+
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 2u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 2u);
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 0u);
+}
+
+// Non-recursive ops bypass the cache entirely: they are answered by the
+// inline getModRef path and never summarized.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, NonRecursiveOpNotSummarized) {
+ mlir::Value a = createAlloca();
+ mlir::Value cst = mlir::arith::ConstantOp::create(
+ *builder, getLoc(), builder->getF32FloatAttr(0.0f));
+ auto store = fir::StoreOp::create(*builder, getLoc(), cst, a);
+ ASSERT_FALSE(store->hasTrait<mlir::OpTrait::HasRecursiveMemoryEffects>());
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis aa{cache};
+
+ EXPECT_TRUE(aa.getModRef(store, a).isMod());
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 0u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 0u);
+}
+
+// clear() drops every summary, so the next query is a miss again. This is the
+// escape hatch a client uses after mutating a summarized op's body.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, ClearDropsSummaries) {
+ mlir::Value a = createAlloca();
+ fir::DoLoopOp loop = createLoopStoringTo(a);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis aa{cache};
+
+ (void)aa.getModRef(loop, a);
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 1u);
+
+ cache.clear();
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 0u);
+
+ (void)aa.getModRef(loop, a);
+ EXPECT_EQ(cache.getSummaryCacheSizeForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 2u);
+}
+
+// The point of the cache is to be invisible: for every (op, location) pair it
+// must produce exactly what an uncached AliasAnalysis produces.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, MatchesUncachedResult) {
+ mlir::Value a = createAlloca();
+ mlir::Value b = createAlloca();
+ fir::DoLoopOp loopA = createLoopStoringTo(a);
+ fir::DoLoopOp loopB = createLoopStoringTo(b);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis cachedAA{cache};
+ fir::AliasAnalysis uncachedAA;
+
+ for (mlir::Operation *op : {loopA.getOperation(), loopB.getOperation()}) {
+ for (mlir::Value loc : {a, b}) {
+ mlir::ModRefResult cached = cachedAA.getModRef(op, loc);
+ mlir::ModRefResult uncached = uncachedAA.getModRef(op, loc);
+ EXPECT_EQ(cached, uncached);
+ // Query again to exercise the hit path, which must agree too.
+ EXPECT_EQ(cachedAA.getModRef(op, loc), uncached);
+ }
+ }
+
+ // A loop that only writes `a` modifies `a` and not `b`.
+ EXPECT_TRUE(cachedAA.getModRef(loopA, a).isMod());
+ EXPECT_TRUE(cachedAA.getModRef(loopA, b).isNoModRef());
+}
+
+// The cache holds a back-pointer to its AliasAnalysis, and AliasAnalysis holds
+// one to the cache. Moving the analysis must re-link both, or the moved-to
+// instance would silently stop caching (or, worse, the cache would delegate
+// through a dangling pointer).
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, MoveKeepsCacheLinked) {
+ mlir::Value a = createAlloca();
+ fir::DoLoopOp loop = createLoopStoringTo(a);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis original{cache};
+ (void)original.getModRef(loop, a);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 1u);
+
+ fir::AliasAnalysis moved{std::move(original)};
+ (void)moved.getModRef(loop, a);
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 1u);
+ EXPECT_EQ(cache.getSummaryCacheMissesForTesting(), 1u);
+}
+
+// The two caches are independent opt-ins serving
diff erent queries:
+// enableSourceCache() memoizes getSource(), the recursive cache memoizes
+// nested-effect summaries. LoopInvariantCodeMotion turns both on, so make sure
+// they compose without interfering.
+TEST_F(AliasAnalysisRecursiveEffectsCacheTest, ComposesWithSourceCache) {
+ mlir::Value a = createAlloca();
+ fir::DoLoopOp loop = createLoopStoringTo(a);
+
+ fir::AliasAnalysisRecursiveEffectsCache cache;
+ fir::AliasAnalysis aa{cache};
+ aa.enableSourceCache();
+
+ mlir::ModRefResult first = aa.getModRef(loop, a);
+ mlir::ModRefResult second = aa.getModRef(loop, a);
+ EXPECT_EQ(first, second);
+
+ EXPECT_EQ(cache.getSummaryCacheHitsForTesting(), 1u);
+ // The summary hit still resolves locations through alias(), which goes
+ // through getSource(), so the source cache is exercised as well.
+ EXPECT_GT(aa.getSourceCacheSizeForTesting(), 0u);
+}
diff --git a/flang/unittests/Optimizer/CMakeLists.txt b/flang/unittests/Optimizer/CMakeLists.txt
index 0e0c12b21bb56..51610776147d0 100644
--- a/flang/unittests/Optimizer/CMakeLists.txt
+++ b/flang/unittests/Optimizer/CMakeLists.txt
@@ -41,6 +41,7 @@ add_flang_unittest(FlangOptimizerTests
OpenACC/FIROpenACCPointerLikeTypeInterfaceTest.cpp
OpenACC/FIROpenACCSupportAnalysisTest.cpp
AliasAnalysisCacheTest.cpp
+ AliasAnalysisRecursiveEffectsCacheTest.cpp
FIRCallInterfaceTest.cpp
FIRContextTest.cpp
FIRTypesTest.cpp
More information about the flang-commits
mailing list