[flang-commits] [flang] [mlir] [RFC][mlir] Resource hierarchy for MLIR Side Effects. (PR #181229)
Slava Zakharin via flang-commits
flang-commits at lists.llvm.org
Mon Mar 9 09:30:01 PDT 2026
https://github.com/vzakhari updated https://github.com/llvm/llvm-project/pull/181229
>From 0995662c53bfe18944f633de3366f9af6a8ffaf7 Mon Sep 17 00:00:00 2001
From: Slava Zakharin <szakharin at nvidia.com>
Date: Thu, 12 Feb 2026 11:45:26 -0800
Subject: [PATCH 1/2] [RFC][mlir] Memory region hierarchy for MLIR Side
Effects.
This patch adds a `MemoryRegion` hierarchy to MLIR's side-effect infrastructure
so that effects on *disjoint* regions (e.g. addressable memory vs. abstract
runtime state) can be proven non-conflicting, unblocking CSE, LICM, and alias
analysis without per-pass special-casing.
---
.../include/flang/Optimizer/Dialect/FIROps.h | 6 +
mlir/include/mlir/Dialect/OpenACC/OpenACC.h | 9 ++
.../mlir/Interfaces/SideEffectInterfaces.h | 117 +++++++++++++++++-
.../AliasAnalysis/LocalAliasAnalysis.cpp | 12 +-
mlir/lib/Transforms/CSE.cpp | 24 +++-
.../Analysis/test-alias-analysis-modref.mlir | 24 ++++
mlir/test/Dialect/OpenACC/cse.mlir | 18 +++
mlir/test/Transforms/cse.mlir | 80 ++++++++++++
mlir/test/lib/Dialect/Test/TestOpDefs.cpp | 12 ++
mlir/test/lib/Dialect/Test/TestOps.h | 49 ++++++++
10 files changed, 344 insertions(+), 7 deletions(-)
create mode 100644 mlir/test/Dialect/OpenACC/cse.mlir
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.h b/flang/include/flang/Optimizer/Dialect/FIROps.h
index 3d08e65524cf1..ff96b31cd060a 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.h
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.h
@@ -51,12 +51,18 @@ static constexpr llvm::StringRef getNormalizedLowerBoundAttrName() {
struct DebuggingResource
: public mlir::SideEffects::Resource::Base<DebuggingResource> {
mlir::StringRef getName() final { return "DebuggingResource"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
};
/// Model operations which read from/write to volatile memory
struct VolatileMemoryResource
: public mlir::SideEffects::Resource::Base<VolatileMemoryResource> {
mlir::StringRef getName() final { return "VolatileMemoryResource"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
};
class CoordinateIndicesAdaptor;
diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACC.h b/mlir/include/mlir/Dialect/OpenACC/OpenACC.h
index 2f6d1ee5bdd3e..69723898e3121 100644
--- a/mlir/include/mlir/Dialect/OpenACC/OpenACC.h
+++ b/mlir/include/mlir/Dialect/OpenACC/OpenACC.h
@@ -212,16 +212,25 @@ static constexpr StringLiteral getCombinedConstructsAttrName() {
struct RuntimeCounters
: public mlir::SideEffects::Resource::Base<RuntimeCounters> {
mlir::StringRef getName() final { return "AccRuntimeCounters"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
};
struct ConstructResource
: public mlir::SideEffects::Resource::Base<ConstructResource> {
mlir::StringRef getName() final { return "AccConstructResource"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
};
struct CurrentDeviceIdResource
: public mlir::SideEffects::Resource::Base<CurrentDeviceIdResource> {
mlir::StringRef getName() final { return "AccCurrentDeviceIdResource"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
};
} // namespace acc
diff --git a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
index 9de20f0c69f1a..b6d81b3c80d45 100644
--- a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
+++ b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
@@ -70,6 +70,91 @@ class Effect {
TypeID id;
};
+//===----------------------------------------------------------------------===//
+// Memory Regions
+//===----------------------------------------------------------------------===//
+
+/// Represents a memory region in a hierarchy. Two regions are disjoint if
+/// neither is an ancestor of the other, meaning operations scoped to disjoint
+/// regions cannot conflict.
+class MemoryRegion {
+public:
+ virtual ~MemoryRegion() = default;
+
+ /// This base class is used for derived regions that are non-parametric.
+ template <typename DerivedRegion, typename BaseRegion = MemoryRegion>
+ class Base : public BaseRegion {
+ public:
+ using BaseT = Base<DerivedRegion>;
+
+ /// Returns a unique singleton instance for the given region class.
+ static DerivedRegion *get() {
+ static DerivedRegion instance;
+ return &instance;
+ }
+
+ /// Return the unique identifier for this region class.
+ static TypeID getRegionID() { return TypeID::get<DerivedRegion>(); }
+
+ /// 'classof' used to support llvm style cast functionality.
+ static bool classof(const MemoryRegion *region) {
+ return region->getRegionID() == BaseT::getRegionID();
+ }
+
+ protected:
+ Base() : BaseRegion(BaseT::getRegionID()) {}
+ };
+
+ /// Return the unique identifier for this region.
+ TypeID getRegionID() const { return id; }
+
+ /// Return a human-readable name for this region.
+ virtual StringRef getName() const = 0;
+
+ /// Return the parent region in the hierarchy, or nullptr for the root.
+ virtual MemoryRegion *getParent() const { return nullptr; }
+
+ /// Returns true if this region is a subregion of (or equal to) another.
+ bool isSubregionOf(const MemoryRegion *other) const {
+ for (const MemoryRegion *r = this; r != nullptr; r = r->getParent())
+ if (r->getRegionID() == other->getRegionID())
+ return true;
+ return false;
+ }
+
+ /// Returns true if this region is disjoint from another region.
+ /// Two regions are disjoint if neither is an ancestor of the other.
+ bool isDisjointFrom(const MemoryRegion *other) const {
+ return !isSubregionOf(other) && !other->isSubregionOf(this);
+ }
+
+protected:
+ MemoryRegion(TypeID id) : id(id) {}
+
+private:
+ TypeID id;
+};
+
+/// Root of the memory region hierarchy. All other regions are subregions of
+/// this. An effect on AllMemory may conflict with any other effect.
+struct AllMemory : public MemoryRegion::Base<AllMemory> {
+ StringRef getName() const override { return "AllMemory"; }
+};
+
+/// Memory that can be accessed via pointers/Values. This is the default region
+/// for resources that don't specify one (conservative assumption).
+struct AddressableMemory : public MemoryRegion::Base<AddressableMemory> {
+ StringRef getName() const override { return "AddressableMemory"; }
+ MemoryRegion *getParent() const override { return AllMemory::get(); }
+};
+
+/// Memory that cannot be accessed via any pointer. Effects on
+/// NonAddressableMemory are disjoint from effects on AddressableMemory.
+struct NonAddressableMemory : public MemoryRegion::Base<NonAddressableMemory> {
+ StringRef getName() const override { return "NonAddressableMemory"; }
+ MemoryRegion *getParent() const override { return AllMemory::get(); }
+};
+
//===----------------------------------------------------------------------===//
// Resources
//===----------------------------------------------------------------------===//
@@ -110,6 +195,13 @@ class Resource {
/// Return a string name of the resource.
virtual StringRef getName() = 0;
+ /// Return the memory region this resource belongs to. Defaults to
+ /// AddressableMemory (conservative: assumes pointer-based access is
+ /// possible). Override in derived resources to specify a different region.
+ virtual MemoryRegion *getMemoryRegion() const {
+ return AddressableMemory::get();
+ }
+
protected:
Resource(TypeID id) : id(id) {}
@@ -152,7 +244,9 @@ class EffectInstance {
EffectInstance(EffectT *effect, T value,
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value), stage(0),
- effectOnFullRegion(false) {}
+ effectOnFullRegion(false) {
+ checkNonAddressableValue();
+ }
template <typename T,
std::enable_if_t<
llvm::is_one_of<T, OpOperand *, OpResult, BlockArgument>::value,
@@ -160,7 +254,9 @@ class EffectInstance {
EffectInstance(EffectT *effect, T value, int stage, bool effectOnFullRegion,
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value), stage(stage),
- effectOnFullRegion(effectOnFullRegion) {}
+ effectOnFullRegion(effectOnFullRegion) {
+ checkNonAddressableValue();
+ }
EffectInstance(EffectT *effect, SymbolRefAttr symbol,
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(symbol), stage(0),
@@ -186,7 +282,9 @@ class EffectInstance {
EffectInstance(EffectT *effect, T value, Attribute parameters,
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value),
- parameters(parameters), stage(0), effectOnFullRegion(false) {}
+ parameters(parameters), stage(0), effectOnFullRegion(false) {
+ checkNonAddressableValue();
+ }
template <typename T,
std::enable_if_t<
llvm::is_one_of<T, OpOperand *, OpResult, BlockArgument>::value,
@@ -196,7 +294,9 @@ class EffectInstance {
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value),
parameters(parameters), stage(stage),
- effectOnFullRegion(effectOnFullRegion) {}
+ effectOnFullRegion(effectOnFullRegion) {
+ checkNonAddressableValue();
+ }
EffectInstance(EffectT *effect, SymbolRefAttr symbol, Attribute parameters,
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(symbol),
@@ -256,6 +356,15 @@ class EffectInstance {
bool getEffectOnFullRegion() const { return effectOnFullRegion; }
private:
+ /// Check that a Value is not associated with a non-addressable resource.
+ void checkNonAddressableValue() {
+ if (resource &&
+ resource->getMemoryRegion()->isSubregionOf(NonAddressableMemory::get()))
+ llvm::report_fatal_error("EffectInstance: non-addressable resource '" +
+ resource->getName() +
+ "' cannot have an associated Value");
+ }
+
/// The specific effect being applied.
EffectT *effect;
diff --git a/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp b/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp
index 5a4679ef31422..c72c386a7ffa2 100644
--- a/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp
+++ b/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp
@@ -525,7 +525,17 @@ ModRefResult LocalAliasAnalysis::getModRef(Operation *op, Value location) {
: aliasResult.isNo() ? "NoAlias"
: "MayAlias");
} else {
- LDBG() << " No effect value, assuming MayAlias";
+ // If the effect's resource is in a memory region disjoint from
+ // AddressableMemory, it cannot alias a location accessible
+ // through a pointer.
+ auto *effectRegion = effect.getResource()->getMemoryRegion();
+ if (effectRegion->isDisjointFrom(SideEffects::AddressableMemory::get())) {
+ LDBG() << " Effect on non-addressable region '"
+ << effectRegion->getName() << "', skipping (NoAlias)";
+ aliasResult = AliasResult::NoAlias;
+ } else {
+ LDBG() << " No effect value, assuming MayAlias";
+ }
}
// If we don't alias, ignore this effect.
diff --git a/mlir/lib/Transforms/CSE.cpp b/mlir/lib/Transforms/CSE.cpp
index 8eaac308755fd..90d0a895c8ff9 100644
--- a/mlir/lib/Transforms/CSE.cpp
+++ b/mlir/lib/Transforms/CSE.cpp
@@ -181,6 +181,18 @@ bool CSEDriver::hasOtherSideEffectingOpInBetween(Operation *fromOp,
"expected read effect on fromOp");
assert(hasEffect<MemoryEffects::Read>(toOp) &&
"expected read effect on toOp");
+
+ // Collect the memory regions of fromOp's read effects. A write can only
+ // block CSE if its resource region is not disjoint from one of these.
+ SmallPtrSet<SideEffects::MemoryRegion *, 1> readRegions;
+ if (auto memOp = dyn_cast<MemoryEffectOpInterface>(fromOp)) {
+ SmallVector<MemoryEffects::EffectInstance> fromEffects;
+ memOp.getEffects(fromEffects);
+ for (const auto &e : fromEffects)
+ if (isa<MemoryEffects::Read>(e.getEffect()))
+ readRegions.insert(e.getResource()->getMemoryRegion());
+ }
+
Operation *nextOp = fromOp->getNextNode();
auto result =
memEffectsCache.try_emplace(fromOp, std::make_pair(fromOp, nullptr));
@@ -210,8 +222,16 @@ bool CSEDriver::hasOtherSideEffectingOpInBetween(Operation *fromOp,
for (const MemoryEffects::EffectInstance &effect : *effects) {
if (isa<MemoryEffects::Write>(effect.getEffect())) {
- result.first->second = {nextOp, MemoryEffects::Write::get()};
- return true;
+ // A write on a memory region disjoint from all read regions cannot
+ // conflict with the reads being CSE'd.
+ auto *writeRegion = effect.getResource()->getMemoryRegion();
+ bool canConflict = llvm::any_of(readRegions, [&](auto *readRegion) {
+ return !writeRegion->isDisjointFrom(readRegion);
+ });
+ if (canConflict) {
+ result.first->second = {nextOp, MemoryEffects::Write::get()};
+ return true;
+ }
}
}
nextOp = nextOp->getNextNode();
diff --git a/mlir/test/Analysis/test-alias-analysis-modref.mlir b/mlir/test/Analysis/test-alias-analysis-modref.mlir
index eee8ae9049cfa..bdc2e30d307d5 100644
--- a/mlir/test/Analysis/test-alias-analysis-modref.mlir
+++ b/mlir/test/Analysis/test-alias-analysis-modref.mlir
@@ -65,3 +65,27 @@ func.func @unknown(%arg0: memref<i32>) attributes {test.ptr = "func"} {
"foo.op"() {test.ptr = "unknown"} : () -> ()
return
}
+
+// -----
+
+// An op writing to a non-addressable resource cannot modify or reference
+// a Value-based (addressable) memory location → NoModRef.
+// CHECK-LABEL: Testing : "nonaddressable_write"
+// CHECK-DAG: side_effect_op -> func.region0#0: NoModRef
+// CHECK-DAG: return -> func.region0#0: NoModRef
+func.func @nonaddressable_write(%arg: memref<2xf32>) attributes {test.ptr = "func"} {
+ "test.side_effect_op"() {effects = [{effect="write", test_nonaddressable_resource}], test.ptr = "side_effect_op"} : () -> i32
+ return {test.ptr = "return"}
+}
+
+// -----
+
+// An op writing to an addressable resource (DefaultResource) without an
+// associated Value → MayAlias → Mod.
+// CHECK-LABEL: Testing : "addressable_write"
+// CHECK-DAG: side_effect_op -> func.region0#0: Mod
+// CHECK-DAG: return -> func.region0#0: NoModRef
+func.func @addressable_write(%arg: memref<2xf32>) attributes {test.ptr = "func"} {
+ "test.side_effect_op"() {effects = [{effect="write"}], test.ptr = "side_effect_op"} : () -> i32
+ return {test.ptr = "return"}
+}
diff --git a/mlir/test/Dialect/OpenACC/cse.mlir b/mlir/test/Dialect/OpenACC/cse.mlir
new file mode 100644
index 0000000000000..97bd1302b966b
--- /dev/null
+++ b/mlir/test/Dialect/OpenACC/cse.mlir
@@ -0,0 +1,18 @@
+// RUN: mlir-opt %s -pass-pipeline='builtin.module(func.func(cse))' | FileCheck %s
+
+// Verify that acc.set (which writes CurrentDeviceIdResource, a non-addressable
+// resource) does not block CSE of identical memref.load operations (which
+// read DefaultResource, an addressable resource). The two resource regions are
+// disjoint, so the write cannot conflict with the loads.
+
+// CHECK-LABEL: @cse_across_acc_set
+func.func @cse_across_acc_set(%a: memref<10xf32>, %i: index) -> (f32, f32) {
+ %v1 = memref.load %a[%i] : memref<10xf32>
+ %c42 = arith.constant 42 : i32
+ acc.set device_num(%c42 : i32)
+ %v2 = memref.load %a[%i] : memref<10xf32>
+ // CHECK: %[[V:.*]] = memref.load
+ // CHECK-NOT: memref.load
+ // CHECK: return %[[V]], %[[V]] : f32, f32
+ return %v1, %v2 : f32, f32
+}
diff --git a/mlir/test/Transforms/cse.mlir b/mlir/test/Transforms/cse.mlir
index b447094874d01..1a150daa8e28d 100644
--- a/mlir/test/Transforms/cse.mlir
+++ b/mlir/test/Transforms/cse.mlir
@@ -573,3 +573,83 @@ func.func @cse_recursive_effects_failure() -> (i32, i32, i32) {
%2 = "test.op_with_memread"() : () -> (i32)
return %0, %2, %1 : i32, i32, i32
}
+
+// -----
+
+/// Check that a write on a non-addressable resource does not block CSE of
+/// reads on the default (addressable) resource, because the regions are
+/// disjoint.
+// CHECK-LABEL: @cse_non_addressable_write_does_not_block
+func.func @cse_non_addressable_write_does_not_block() -> i32 {
+ // CHECK-NEXT: %[[V:.*]] = "test.op_with_memread"() : () -> i32
+ %0 = "test.op_with_memread"() : () -> (i32)
+ "test.side_effect_op"() {effects = [{effect="write", test_nonaddressable_resource}]} : () -> i32
+ %1 = "test.op_with_memread"() : () -> (i32)
+ // CHECK-NEXT: %{{.*}} = "test.side_effect_op"()
+ // CHECK-NEXT: %{{.*}} = arith.addi %[[V]], %[[V]] : i32
+ %2 = arith.addi %0, %1 : i32
+ return %2 : i32
+}
+
+// -----
+
+/// Check that consecutive reads on the same non-addressable resource are CSE'd
+/// when there is no intervening write.
+// CHECK-LABEL: @cse_reads_on_same_nonaddressable_resource
+func.func @cse_reads_on_same_nonaddressable_resource() -> i32 {
+ // CHECK-NEXT: %[[V:.*]] = "test.side_effect_op"()
+ %0 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource}]} : () -> i32
+ %1 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource}]} : () -> i32
+ // CHECK-NEXT: %{{.*}} = arith.addi %[[V]], %[[V]] : i32
+ %2 = arith.addi %0, %1 : i32
+ return %2 : i32
+}
+
+// -----
+
+/// Check that a write to the same non-addressable region blocks CSE of reads
+/// on that region (same region → not disjoint → write may conflict).
+// CHECK-LABEL: @cse_write_same_nonaddressable_region_blocks
+func.func @cse_write_same_nonaddressable_region_blocks() -> i32 {
+ // CHECK-NEXT: %[[V0:.*]] = "test.side_effect_op"(){{.*}}"read"
+ %0 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource}]} : () -> i32
+ "test.side_effect_op"() {effects = [{effect="write", test_nonaddressable_resource}]} : () -> i32
+ // CHECK: %[[V1:.*]] = "test.side_effect_op"(){{.*}}"read"
+ %1 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource}]} : () -> i32
+ // CHECK: %{{.*}} = arith.addi %[[V0]], %[[V1]] : i32
+ %2 = arith.addi %0, %1 : i32
+ return %2 : i32
+}
+
+// -----
+
+/// Check that a write to a disjoint non-addressable sub-region does NOT block
+/// CSE of reads on a different non-addressable sub-region (sibling regions
+/// under NonAddressableMemory are disjoint).
+// CHECK-LABEL: @cse_write_disjoint_nonaddressable_subregion_allows
+func.func @cse_write_disjoint_nonaddressable_subregion_allows() -> i32 {
+ // CHECK-NEXT: %[[V:.*]] = "test.side_effect_op"()
+ %0 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource_a}]} : () -> i32
+ "test.side_effect_op"() {effects = [{effect="write", test_nonaddressable_resource_b}]} : () -> i32
+ %1 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource_a}]} : () -> i32
+ // CHECK-NEXT: %{{.*}} = "test.side_effect_op"()
+ // CHECK-NEXT: %{{.*}} = arith.addi %[[V]], %[[V]] : i32
+ %2 = arith.addi %0, %1 : i32
+ return %2 : i32
+}
+
+// -----
+
+/// Check that a write on an addressable custom resource still blocks CSE of
+/// reads on the default (addressable) resource (regression guard).
+// CHECK-LABEL: @cse_addressable_custom_resource_write_blocks
+func.func @cse_addressable_custom_resource_write_blocks() -> i32 {
+ // CHECK-NEXT: %[[V0:.*]] = "test.op_with_memread"() : () -> i32
+ %0 = "test.op_with_memread"() : () -> (i32)
+ "test.side_effect_op"() {effects = [{effect="write", test_resource}]} : () -> i32
+ // CHECK: %[[V1:.*]] = "test.op_with_memread"() : () -> i32
+ %1 = "test.op_with_memread"() : () -> (i32)
+ // CHECK: %{{.*}} = arith.addi %[[V0]], %[[V1]] : i32
+ %2 = arith.addi %0, %1 : i32
+ return %2 : i32
+}
diff --git a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
index d2826cc42b270..0889ea0aace98 100644
--- a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
+++ b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
@@ -479,6 +479,12 @@ void SideEffectOp::getEffects(
SideEffects::Resource *resource = SideEffects::DefaultResource::get();
if (effectElement.get("test_resource"))
resource = TestResource::get();
+ else if (effectElement.get("test_nonaddressable_resource"))
+ resource = TestNonAddressableResource::get();
+ else if (effectElement.get("test_nonaddressable_resource_a"))
+ resource = TestNonAddressableResourceA::get();
+ else if (effectElement.get("test_nonaddressable_resource_b"))
+ resource = TestNonAddressableResourceB::get();
// Check for a result to affect.
if (effectElement.get("on_result"))
@@ -518,6 +524,12 @@ void SideEffectWithRegionOp::getEffects(
SideEffects::Resource *resource = SideEffects::DefaultResource::get();
if (effectElement.get("test_resource"))
resource = TestResource::get();
+ else if (effectElement.get("test_nonaddressable_resource"))
+ resource = TestNonAddressableResource::get();
+ else if (effectElement.get("test_nonaddressable_resource_a"))
+ resource = TestNonAddressableResourceA::get();
+ else if (effectElement.get("test_nonaddressable_resource_b"))
+ resource = TestNonAddressableResourceB::get();
// Check for a result to affect.
if (effectElement.get("on_result"))
diff --git a/mlir/test/lib/Dialect/Test/TestOps.h b/mlir/test/lib/Dialect/Test/TestOps.h
index 679274346fb13..8b39699beb70a 100644
--- a/mlir/test/lib/Dialect/Test/TestOps.h
+++ b/mlir/test/lib/Dialect/Test/TestOps.h
@@ -56,6 +56,55 @@ struct TestResource : public mlir::SideEffects::Resource::Base<TestResource> {
llvm::StringRef getName() final { return "<Test>"; }
};
+/// A test resource in NonAddressableMemory (disjoint from AddressableMemory).
+struct TestNonAddressableResource
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableResource> {
+ llvm::StringRef getName() final { return "<TestNonAddressable>"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
+};
+
+/// Two disjoint sub-regions under NonAddressableMemory for testing sibling
+/// disjointness within non-addressable memory.
+struct TestNonAddressableSubRegionA
+ : public mlir::SideEffects::MemoryRegion::Base<
+ TestNonAddressableSubRegionA> {
+ llvm::StringRef getName() const override {
+ return "TestNonAddressableSubRegionA";
+ }
+ mlir::SideEffects::MemoryRegion *getParent() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
+};
+
+struct TestNonAddressableSubRegionB
+ : public mlir::SideEffects::MemoryRegion::Base<
+ TestNonAddressableSubRegionB> {
+ llvm::StringRef getName() const override {
+ return "TestNonAddressableSubRegionB";
+ }
+ mlir::SideEffects::MemoryRegion *getParent() const override {
+ return mlir::SideEffects::NonAddressableMemory::get();
+ }
+};
+
+struct TestNonAddressableResourceA
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableResourceA> {
+ llvm::StringRef getName() final { return "<TestNonAddressableA>"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return TestNonAddressableSubRegionA::get();
+ }
+};
+
+struct TestNonAddressableResourceB
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableResourceB> {
+ llvm::StringRef getName() final { return "<TestNonAddressableB>"; }
+ mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
+ return TestNonAddressableSubRegionB::get();
+ }
+};
+
//===----------------------------------------------------------------------===//
// PropertiesWithCustomPrint
//===----------------------------------------------------------------------===//
>From ccfd82bee68344cb096a2043fb669c1197ffc323 Mon Sep 17 00:00:00 2001
From: Slava Zakharin <szakharin at nvidia.com>
Date: Fri, 20 Feb 2026 12:21:06 -0800
Subject: [PATCH 2/2] =?UTF-8?q?Addressed=20review=20comments:=20*=20No=20s?=
=?UTF-8?q?eparate=20MemoryRegion=20=E2=80=94=20The=20MemoryRegion=20tree?=
=?UTF-8?q?=20(AllMemory,=20AddressableMemory,=20et.c)=20and=20Resource::g?=
=?UTF-8?q?etMemoryRegion()=20are=20removed.=20Only=20the=20Resource=20hie?=
=?UTF-8?q?rarchy=20remains,=20with=20getParent(),=20isSubresourceOf(),=20?=
=?UTF-8?q?and=20isDisjointFrom()=20on=20Resource.=20CSE=20and=20alias=20a?=
=?UTF-8?q?nalysis=20use=20resource=20disjointness=20instead=20of=20region?=
=?UTF-8?q?=20disjointness.=20*=20Addressability=20on=20Resource=20?=
=?UTF-8?q?=E2=80=94=20"Non-addressable"=20is=20a=20property=20of=20a=20re?=
=?UTF-8?q?source:=20Resource::isAddressable()=20(default=20true).=20Diale?=
=?UTF-8?q?cts=20that=20were=20"in=20NonAddressableMemory"=20now=20overrid?=
=?UTF-8?q?e=20isAddressable()=20to=20false=20and=20no=20longer=20implemen?=
=?UTF-8?q?t=20getMemoryRegion().=20EffectInstance=20still=20forbids=20att?=
=?UTF-8?q?aching=20a=20Value=20to=20a=20resource=20disjoint=20from=20Defa?=
=?UTF-8?q?ultResource;=20an=20addressable=20resource=20may=20not=20have?=
=?UTF-8?q?=20a=20non-addressable=20parent=20(via=20verifyImmediateParentA?=
=?UTF-8?q?ddressability()=20under=20EXPENSIVE=5FCHECKS).=20*=20Added=20su?=
=?UTF-8?q?pport=20for=20isa/cast=20(note=20that=20cast<DefaultResource>(A?=
=?UTF-8?q?utomaticAllocationScopeResource::get())=20still=20returns=20Aut?=
=?UTF-8?q?omaticAllocationScopeResource::get()=20singleton).=20*=20Docs?=
=?UTF-8?q?=20and=20tests=20=E2=80=94=20Scope=20and=20limitations=20are=20?=
=?UTF-8?q?documented=20(see=20header=20and=20SideEffectsAndSpeculation.md?=
=?UTF-8?q?).=20New=20unit=20test=20for=20Resource=20isa/cast=20and=20hier?=
=?UTF-8?q?archy.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../include/flang/Optimizer/Dialect/FIROps.h | 12 +-
.../Rationale/SideEffectsAndSpeculation.md | 16 ++
mlir/include/mlir/Dialect/OpenACC/OpenACC.h | 18 +-
.../Interfaces/TransformInterfaces.h | 4 +-
.../mlir/Interfaces/SideEffectInterfaces.h | 191 ++++++++----------
.../AliasAnalysis/LocalAliasAnalysis.cpp | 12 +-
mlir/lib/Transforms/CSE.cpp | 16 +-
.../Analysis/test-alias-analysis-modref.mlir | 8 +-
mlir/test/Dialect/OpenACC/cse.mlir | 8 +-
mlir/test/Transforms/cse.mlir | 28 ++-
mlir/test/lib/Dialect/Test/TestOpDefs.cpp | 2 +-
mlir/test/lib/Dialect/Test/TestOps.h | 71 ++++---
mlir/unittests/Interfaces/CMakeLists.txt | 2 +
.../Interfaces/SideEffectInterfacesTest.cpp | 123 +++++++++++
14 files changed, 308 insertions(+), 203 deletions(-)
create mode 100644 mlir/unittests/Interfaces/SideEffectInterfacesTest.cpp
diff --git a/flang/include/flang/Optimizer/Dialect/FIROps.h b/flang/include/flang/Optimizer/Dialect/FIROps.h
index ff96b31cd060a..9d771b1e60604 100644
--- a/flang/include/flang/Optimizer/Dialect/FIROps.h
+++ b/flang/include/flang/Optimizer/Dialect/FIROps.h
@@ -50,19 +50,15 @@ static constexpr llvm::StringRef getNormalizedLowerBoundAttrName() {
/// Model operations which affect global debugging information
struct DebuggingResource
: public mlir::SideEffects::Resource::Base<DebuggingResource> {
- mlir::StringRef getName() final { return "DebuggingResource"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
- }
+ mlir::StringRef getName() const final { return "DebuggingResource"; }
+ bool isAddressable() const override { return false; }
};
/// Model operations which read from/write to volatile memory
struct VolatileMemoryResource
: public mlir::SideEffects::Resource::Base<VolatileMemoryResource> {
- mlir::StringRef getName() final { return "VolatileMemoryResource"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
- }
+ mlir::StringRef getName() const final { return "VolatileMemoryResource"; }
+ bool isAddressable() const override { return false; }
};
class CoordinateIndicesAdaptor;
diff --git a/mlir/docs/Rationale/SideEffectsAndSpeculation.md b/mlir/docs/Rationale/SideEffectsAndSpeculation.md
index 4d9021a356dfe..f2e9e12f6cfc9 100644
--- a/mlir/docs/Rationale/SideEffectsAndSpeculation.md
+++ b/mlir/docs/Rationale/SideEffectsAndSpeculation.md
@@ -75,6 +75,22 @@ memory effects they have and whether they are speculatable.
We don't have proper modeling yet to fully capture non-local control flow
semantics.
+### Resource hierarchy and scope
+
+Each effect is applied to a `Resource`. Resources form a hierarchy: each
+resource may have a parent (`getParent()`), and the API provides
+`isSubresourceOf()` and `isDisjointFrom()` so that passes can determine whether
+two effects may conflict (e.g. CSE and alias analysis use disjointness to allow
+more optimizations). A resource may be *addressable* (`isAddressable()`), meaning
+effects on it can alias pointer-based memory; non-addressable resources (e.g.
+runtime state) do not alias with any value-based memory location. The
+canonical definition and API live in `mlir/Interfaces/SideEffectInterfaces.h`.
+
+**Scope and limitations.** This mechanism is deliberately *not* intended for
+fine-grained regions with specific addresses or sizes, or for alias classes /
+offset-based disambiguation. Those concerns are out of scope for the resource
+hierarchy and should be handled by alias analysis or other mechanisms.
+
When adding a new op, ask:
1. Does it read from or write to the heap or stack? It should probably implement
diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACC.h b/mlir/include/mlir/Dialect/OpenACC/OpenACC.h
index 69723898e3121..13eda0486f98f 100644
--- a/mlir/include/mlir/Dialect/OpenACC/OpenACC.h
+++ b/mlir/include/mlir/Dialect/OpenACC/OpenACC.h
@@ -211,26 +211,20 @@ static constexpr StringLiteral getCombinedConstructsAttrName() {
struct RuntimeCounters
: public mlir::SideEffects::Resource::Base<RuntimeCounters> {
- mlir::StringRef getName() final { return "AccRuntimeCounters"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
- }
+ mlir::StringRef getName() const final { return "AccRuntimeCounters"; }
+ bool isAddressable() const override { return false; }
};
struct ConstructResource
: public mlir::SideEffects::Resource::Base<ConstructResource> {
- mlir::StringRef getName() final { return "AccConstructResource"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
- }
+ mlir::StringRef getName() const final { return "AccConstructResource"; }
+ bool isAddressable() const override { return false; }
};
struct CurrentDeviceIdResource
: public mlir::SideEffects::Resource::Base<CurrentDeviceIdResource> {
- mlir::StringRef getName() final { return "AccCurrentDeviceIdResource"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
- }
+ mlir::StringRef getName() const final { return "AccCurrentDeviceIdResource"; }
+ bool isAddressable() const override { return false; }
};
} // namespace acc
diff --git a/mlir/include/mlir/Dialect/Transform/Interfaces/TransformInterfaces.h b/mlir/include/mlir/Dialect/Transform/Interfaces/TransformInterfaces.h
index 8b0517a84a675..c2ceaba1128a3 100644
--- a/mlir/include/mlir/Dialect/Transform/Interfaces/TransformInterfaces.h
+++ b/mlir/include/mlir/Dialect/Transform/Interfaces/TransformInterfaces.h
@@ -1256,7 +1256,7 @@ class TransformEachOpTrait
// as CSE/DCE to work.
struct TransformMappingResource
: public SideEffects::Resource::Base<TransformMappingResource> {
- StringRef getName() override { return "transform.mapping"; }
+ StringRef getName() const override { return "transform.mapping"; }
};
/// Side effect resource corresponding to the Payload IR itself. Only Read and
@@ -1267,7 +1267,7 @@ struct TransformMappingResource
/// while still allowing the reordering of those that only access it.
struct PayloadIRResource
: public SideEffects::Resource::Base<PayloadIRResource> {
- StringRef getName() override { return "transform.payload_ir"; }
+ StringRef getName() const override { return "transform.payload_ir"; }
};
/// Populates `effects` with the memory effects indicating the operation on the
diff --git a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
index b6d81b3c80d45..97f860565b183 100644
--- a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
+++ b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
@@ -15,6 +15,7 @@
#define MLIR_INTERFACES_SIDEEFFECTINTERFACES_H
#include "mlir/IR/OpDefinition.h"
+#include "llvm/ADT/Twine.h"
namespace mlir {
namespace SideEffects {
@@ -70,97 +71,20 @@ class Effect {
TypeID id;
};
-//===----------------------------------------------------------------------===//
-// Memory Regions
-//===----------------------------------------------------------------------===//
-
-/// Represents a memory region in a hierarchy. Two regions are disjoint if
-/// neither is an ancestor of the other, meaning operations scoped to disjoint
-/// regions cannot conflict.
-class MemoryRegion {
-public:
- virtual ~MemoryRegion() = default;
-
- /// This base class is used for derived regions that are non-parametric.
- template <typename DerivedRegion, typename BaseRegion = MemoryRegion>
- class Base : public BaseRegion {
- public:
- using BaseT = Base<DerivedRegion>;
-
- /// Returns a unique singleton instance for the given region class.
- static DerivedRegion *get() {
- static DerivedRegion instance;
- return &instance;
- }
-
- /// Return the unique identifier for this region class.
- static TypeID getRegionID() { return TypeID::get<DerivedRegion>(); }
-
- /// 'classof' used to support llvm style cast functionality.
- static bool classof(const MemoryRegion *region) {
- return region->getRegionID() == BaseT::getRegionID();
- }
-
- protected:
- Base() : BaseRegion(BaseT::getRegionID()) {}
- };
-
- /// Return the unique identifier for this region.
- TypeID getRegionID() const { return id; }
-
- /// Return a human-readable name for this region.
- virtual StringRef getName() const = 0;
-
- /// Return the parent region in the hierarchy, or nullptr for the root.
- virtual MemoryRegion *getParent() const { return nullptr; }
-
- /// Returns true if this region is a subregion of (or equal to) another.
- bool isSubregionOf(const MemoryRegion *other) const {
- for (const MemoryRegion *r = this; r != nullptr; r = r->getParent())
- if (r->getRegionID() == other->getRegionID())
- return true;
- return false;
- }
-
- /// Returns true if this region is disjoint from another region.
- /// Two regions are disjoint if neither is an ancestor of the other.
- bool isDisjointFrom(const MemoryRegion *other) const {
- return !isSubregionOf(other) && !other->isSubregionOf(this);
- }
-
-protected:
- MemoryRegion(TypeID id) : id(id) {}
-
-private:
- TypeID id;
-};
-
-/// Root of the memory region hierarchy. All other regions are subregions of
-/// this. An effect on AllMemory may conflict with any other effect.
-struct AllMemory : public MemoryRegion::Base<AllMemory> {
- StringRef getName() const override { return "AllMemory"; }
-};
-
-/// Memory that can be accessed via pointers/Values. This is the default region
-/// for resources that don't specify one (conservative assumption).
-struct AddressableMemory : public MemoryRegion::Base<AddressableMemory> {
- StringRef getName() const override { return "AddressableMemory"; }
- MemoryRegion *getParent() const override { return AllMemory::get(); }
-};
-
-/// Memory that cannot be accessed via any pointer. Effects on
-/// NonAddressableMemory are disjoint from effects on AddressableMemory.
-struct NonAddressableMemory : public MemoryRegion::Base<NonAddressableMemory> {
- StringRef getName() const override { return "NonAddressableMemory"; }
- MemoryRegion *getParent() const override { return AllMemory::get(); }
-};
-
//===----------------------------------------------------------------------===//
// Resources
//===----------------------------------------------------------------------===//
/// This class represents a specific resource that an effect applies to. This
-/// class represents an abstract interface for a given resource.
+/// class represents an abstract interface for a given resource. Resources
+/// form a hierarchy via getParent(); disjointness (isDisjointFrom) is used to
+/// determine whether effects can conflict.
+///
+/// Scope: The resource hierarchy is for disjointness of *abstract* resources
+/// (e.g. addressable memory vs. runtime state). It is deliberately *not*
+/// intended for fine-grained regions with specific addresses/sizes, or for
+/// alias classes / offset-based disambiguation; such concerns are out of scope
+/// and should be handled by alias analysis or other mechanisms.
class Resource {
public:
virtual ~Resource() = default;
@@ -169,7 +93,9 @@ class Resource {
template <typename DerivedResource, typename BaseResource = Resource>
class Base : public BaseResource {
public:
- using BaseT = Base<DerivedResource>;
+ /// Use the current instantiation so get()/getResourceID() refer to this
+ /// hierarchy's singleton, not Base<DerivedResource, Resource>'s.
+ using BaseT = Base<DerivedResource, BaseResource>;
/// Returns a unique instance for the given effect class.
static DerivedResource *get() {
@@ -180,46 +106,92 @@ class Resource {
/// Return the unique identifier for the base resource class.
static TypeID getResourceID() { return TypeID::get<DerivedResource>(); }
- /// 'classof' used to support llvm style cast functionality.
+ /// 'classof' used to support llvm style cast functionality. Returns true
+ /// iff the resource is the same as or a descendant of this resource type
+ /// in the hierarchy (so isa/cast work for ancestor checks).
static bool classof(const Resource *resource) {
- return resource->getResourceID() == BaseT::getResourceID();
+ return resource->isSubresourceOf(BaseT::get());
}
protected:
- Base() : BaseResource(BaseT::getResourceID()){};
+ Base() : BaseResource(BaseT::getResourceID()) {}
+ /// Constructor for use when this type is used as a parent (BaseResource);
+ /// allows the derived resource to pass its TypeID so the hierarchy is
+ /// correct.
+ Base(TypeID id) : BaseResource(id) {}
};
/// Return the unique identifier for the base resource class.
TypeID getResourceID() const { return id; }
/// Return a string name of the resource.
- virtual StringRef getName() = 0;
+ virtual StringRef getName() const = 0;
+
+ /// Return the parent resource in the hierarchy, or nullptr for a root.
+ virtual Resource *getParent() const { return nullptr; }
+
+ /// Returns true if this resource is addressable (effects on it can alias
+ /// pointer-based memory). Default is true.
+ virtual bool isAddressable() const { return true; }
- /// Return the memory region this resource belongs to. Defaults to
- /// AddressableMemory (conservative: assumes pointer-based access is
- /// possible). Override in derived resources to specify a different region.
- virtual MemoryRegion *getMemoryRegion() const {
- return AddressableMemory::get();
+ /// Returns true if this resource is a subresource of (or equal to) another.
+ bool isSubresourceOf(const Resource *other) const {
+ for (const Resource *r = this; r != nullptr; r = r->getParent()) {
+#ifdef EXPENSIVE_CHECKS
+ r->verifyImmediateParentAddressability();
+#endif // EXPENSIVE_CHECKS
+ if (r == other)
+ return true;
+ }
+ return false;
+ }
+
+ /// Returns true if this resource is disjoint from another. Two resources are
+ /// disjoint if neither is an ancestor of the other.
+ bool isDisjointFrom(const Resource *other) const {
+ return !isSubresourceOf(other) && !other->isSubresourceOf(this);
}
protected:
Resource(TypeID id) : id(id) {}
private:
+#ifdef EXPENSIVE_CHECKS
+ /// Verifies the single-link invariant: an addressable resource must not have
+ /// a non-addressable parent. Used from isSubresourceOf() under
+ /// EXPENSIVE_CHECKS so the invariant is checked when the hierarchy is
+ /// traversed.
+ void verifyImmediateParentAddressability() const {
+ Resource *parent = getParent();
+ if (parent && isAddressable() && !parent->isAddressable())
+ llvm::report_fatal_error(
+ llvm::Twine("Resource '") + getName() +
+ "' is addressable but has non-addressable parent '" +
+ parent->getName() + "'");
+ }
+#endif // EXPENSIVE_CHECKS
+
/// The id of the derived resource class.
TypeID id;
};
/// A conservative default resource kind.
struct DefaultResource : public Resource::Base<DefaultResource> {
- StringRef getName() final { return "<Default>"; }
+ DefaultResource() = default;
+ StringRef getName() const override { return "<Default>"; }
+
+protected:
+ /// For use when this type is the parent of another resource; allows the
+ /// derived resource to pass its TypeID so the hierarchy is correct.
+ DefaultResource(TypeID id) : Base(id) {}
};
/// An automatic allocation-scope resource that is valid in the context of a
/// parent AutomaticAllocationScope trait.
struct AutomaticAllocationScopeResource
- : public Resource::Base<AutomaticAllocationScopeResource> {
- StringRef getName() final { return "AutomaticAllocationScope"; }
+ : public Resource::Base<AutomaticAllocationScopeResource, DefaultResource> {
+ StringRef getName() const final { return "AutomaticAllocationScope"; }
+ Resource *getParent() const override { return DefaultResource::get(); }
};
/// This class represents a specific instance of an effect. It contains the
@@ -245,7 +217,7 @@ class EffectInstance {
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value), stage(0),
effectOnFullRegion(false) {
- checkNonAddressableValue();
+ checkResourceAllowsValue();
}
template <typename T,
std::enable_if_t<
@@ -255,7 +227,7 @@ class EffectInstance {
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value), stage(stage),
effectOnFullRegion(effectOnFullRegion) {
- checkNonAddressableValue();
+ checkResourceAllowsValue();
}
EffectInstance(EffectT *effect, SymbolRefAttr symbol,
Resource *resource = DefaultResource::get())
@@ -283,7 +255,7 @@ class EffectInstance {
Resource *resource = DefaultResource::get())
: effect(effect), resource(resource), value(value),
parameters(parameters), stage(0), effectOnFullRegion(false) {
- checkNonAddressableValue();
+ checkResourceAllowsValue();
}
template <typename T,
std::enable_if_t<
@@ -295,7 +267,7 @@ class EffectInstance {
: effect(effect), resource(resource), value(value),
parameters(parameters), stage(stage),
effectOnFullRegion(effectOnFullRegion) {
- checkNonAddressableValue();
+ checkResourceAllowsValue();
}
EffectInstance(EffectT *effect, SymbolRefAttr symbol, Attribute parameters,
Resource *resource = DefaultResource::get())
@@ -356,13 +328,12 @@ class EffectInstance {
bool getEffectOnFullRegion() const { return effectOnFullRegion; }
private:
- /// Check that a Value is not associated with a non-addressable resource.
- void checkNonAddressableValue() {
- if (resource &&
- resource->getMemoryRegion()->isSubregionOf(NonAddressableMemory::get()))
- llvm::report_fatal_error("EffectInstance: non-addressable resource '" +
- resource->getName() +
- "' cannot have an associated Value");
+ /// Effect on a non-addressable resource cannot have an associated Value.
+ void checkResourceAllowsValue() {
+ if (value && resource && !resource->isAddressable())
+ llvm::report_fatal_error(
+ llvm::Twine("EffectInstance: resource '") + resource->getName() +
+ "' is non-addressable and cannot have an associated Value");
}
/// The specific effect being applied.
diff --git a/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp b/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp
index c72c386a7ffa2..11123af49774b 100644
--- a/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp
+++ b/mlir/lib/Analysis/AliasAnalysis/LocalAliasAnalysis.cpp
@@ -525,13 +525,11 @@ ModRefResult LocalAliasAnalysis::getModRef(Operation *op, Value location) {
: aliasResult.isNo() ? "NoAlias"
: "MayAlias");
} else {
- // If the effect's resource is in a memory region disjoint from
- // AddressableMemory, it cannot alias a location accessible
- // through a pointer.
- auto *effectRegion = effect.getResource()->getMemoryRegion();
- if (effectRegion->isDisjointFrom(SideEffects::AddressableMemory::get())) {
- LDBG() << " Effect on non-addressable region '"
- << effectRegion->getName() << "', skipping (NoAlias)";
+ // An effect on a non-addressable resource cannot affect a pointer-based
+ // location.
+ if (!effect.getResource()->isAddressable()) {
+ LDBG() << " Effect on non-addressable resource '"
+ << effect.getResource()->getName() << "', skipping (NoAlias)";
aliasResult = AliasResult::NoAlias;
} else {
LDBG() << " No effect value, assuming MayAlias";
diff --git a/mlir/lib/Transforms/CSE.cpp b/mlir/lib/Transforms/CSE.cpp
index 90d0a895c8ff9..e04b7399cfb44 100644
--- a/mlir/lib/Transforms/CSE.cpp
+++ b/mlir/lib/Transforms/CSE.cpp
@@ -182,15 +182,15 @@ bool CSEDriver::hasOtherSideEffectingOpInBetween(Operation *fromOp,
assert(hasEffect<MemoryEffects::Read>(toOp) &&
"expected read effect on toOp");
- // Collect the memory regions of fromOp's read effects. A write can only
- // block CSE if its resource region is not disjoint from one of these.
- SmallPtrSet<SideEffects::MemoryRegion *, 1> readRegions;
+ // Collect the resources of fromOp's read effects. A write can only block
+ // CSE if its resource is not disjoint from one of these.
+ SmallPtrSet<SideEffects::Resource *, 1> readResources;
if (auto memOp = dyn_cast<MemoryEffectOpInterface>(fromOp)) {
SmallVector<MemoryEffects::EffectInstance> fromEffects;
memOp.getEffects(fromEffects);
for (const auto &e : fromEffects)
if (isa<MemoryEffects::Read>(e.getEffect()))
- readRegions.insert(e.getResource()->getMemoryRegion());
+ readResources.insert(e.getResource());
}
Operation *nextOp = fromOp->getNextNode();
@@ -222,11 +222,11 @@ bool CSEDriver::hasOtherSideEffectingOpInBetween(Operation *fromOp,
for (const MemoryEffects::EffectInstance &effect : *effects) {
if (isa<MemoryEffects::Write>(effect.getEffect())) {
- // A write on a memory region disjoint from all read regions cannot
+ // A write on a resource disjoint from all read resources cannot
// conflict with the reads being CSE'd.
- auto *writeRegion = effect.getResource()->getMemoryRegion();
- bool canConflict = llvm::any_of(readRegions, [&](auto *readRegion) {
- return !writeRegion->isDisjointFrom(readRegion);
+ auto *writeResource = effect.getResource();
+ bool canConflict = llvm::any_of(readResources, [&](auto *readResource) {
+ return !writeResource->isDisjointFrom(readResource);
});
if (canConflict) {
result.first->second = {nextOp, MemoryEffects::Write::get()};
diff --git a/mlir/test/Analysis/test-alias-analysis-modref.mlir b/mlir/test/Analysis/test-alias-analysis-modref.mlir
index bdc2e30d307d5..b82f789f4d77d 100644
--- a/mlir/test/Analysis/test-alias-analysis-modref.mlir
+++ b/mlir/test/Analysis/test-alias-analysis-modref.mlir
@@ -68,8 +68,8 @@ func.func @unknown(%arg0: memref<i32>) attributes {test.ptr = "func"} {
// -----
-// An op writing to a non-addressable resource cannot modify or reference
-// a Value-based (addressable) memory location → NoModRef.
+// An op writing to a non-addressable resource cannot modify or reference a
+// Value-based memory location -> NoModRef.
// CHECK-LABEL: Testing : "nonaddressable_write"
// CHECK-DAG: side_effect_op -> func.region0#0: NoModRef
// CHECK-DAG: return -> func.region0#0: NoModRef
@@ -80,8 +80,8 @@ func.func @nonaddressable_write(%arg: memref<2xf32>) attributes {test.ptr = "fun
// -----
-// An op writing to an addressable resource (DefaultResource) without an
-// associated Value → MayAlias → Mod.
+// An op writing to an addressable resource without an associated Value ->
+// MayAlias -> Mod.
// CHECK-LABEL: Testing : "addressable_write"
// CHECK-DAG: side_effect_op -> func.region0#0: Mod
// CHECK-DAG: return -> func.region0#0: NoModRef
diff --git a/mlir/test/Dialect/OpenACC/cse.mlir b/mlir/test/Dialect/OpenACC/cse.mlir
index 97bd1302b966b..7f3a11eb21e09 100644
--- a/mlir/test/Dialect/OpenACC/cse.mlir
+++ b/mlir/test/Dialect/OpenACC/cse.mlir
@@ -1,9 +1,9 @@
// RUN: mlir-opt %s -pass-pipeline='builtin.module(func.func(cse))' | FileCheck %s
-// Verify that acc.set (which writes CurrentDeviceIdResource, a non-addressable
-// resource) does not block CSE of identical memref.load operations (which
-// read DefaultResource, an addressable resource). The two resource regions are
-// disjoint, so the write cannot conflict with the loads.
+// Verify that acc.set (which writes CurrentDeviceIdResource, a root disjoint
+// from DefaultResource) does not block CSE of identical memref.load operations
+// (which read DefaultResource). The two resources are disjoint, so the write
+// cannot conflict with the loads.
// CHECK-LABEL: @cse_across_acc_set
func.func @cse_across_acc_set(%a: memref<10xf32>, %i: index) -> (f32, f32) {
diff --git a/mlir/test/Transforms/cse.mlir b/mlir/test/Transforms/cse.mlir
index 1a150daa8e28d..14cdcdeffd1ec 100644
--- a/mlir/test/Transforms/cse.mlir
+++ b/mlir/test/Transforms/cse.mlir
@@ -576,9 +576,8 @@ func.func @cse_recursive_effects_failure() -> (i32, i32, i32) {
// -----
-/// Check that a write on a non-addressable resource does not block CSE of
-/// reads on the default (addressable) resource, because the regions are
-/// disjoint.
+/// Check that a write on a resource disjoint from DefaultResource does not
+/// block CSE of reads on DefaultResource, because the resources are disjoint.
// CHECK-LABEL: @cse_non_addressable_write_does_not_block
func.func @cse_non_addressable_write_does_not_block() -> i32 {
// CHECK-NEXT: %[[V:.*]] = "test.op_with_memread"() : () -> i32
@@ -593,7 +592,7 @@ func.func @cse_non_addressable_write_does_not_block() -> i32 {
// -----
-/// Check that consecutive reads on the same non-addressable resource are CSE'd
+/// Check that consecutive reads on the same resource (disjoint from DefaultResource) are CSE'd
/// when there is no intervening write.
// CHECK-LABEL: @cse_reads_on_same_nonaddressable_resource
func.func @cse_reads_on_same_nonaddressable_resource() -> i32 {
@@ -607,10 +606,10 @@ func.func @cse_reads_on_same_nonaddressable_resource() -> i32 {
// -----
-/// Check that a write to the same non-addressable region blocks CSE of reads
-/// on that region (same region → not disjoint → write may conflict).
-// CHECK-LABEL: @cse_write_same_nonaddressable_region_blocks
-func.func @cse_write_same_nonaddressable_region_blocks() -> i32 {
+/// Check that a write to the same resource blocks CSE of reads
+/// on that resource (same resource -> not disjoint -> write may conflict).
+// CHECK-LABEL: @cse_write_same_nonaddressable_resource_blocks
+func.func @cse_write_same_nonaddressable_resource_blocks() -> i32 {
// CHECK-NEXT: %[[V0:.*]] = "test.side_effect_op"(){{.*}}"read"
%0 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource}]} : () -> i32
"test.side_effect_op"() {effects = [{effect="write", test_nonaddressable_resource}]} : () -> i32
@@ -623,11 +622,10 @@ func.func @cse_write_same_nonaddressable_region_blocks() -> i32 {
// -----
-/// Check that a write to a disjoint non-addressable sub-region does NOT block
-/// CSE of reads on a different non-addressable sub-region (sibling regions
-/// under NonAddressableMemory are disjoint).
-// CHECK-LABEL: @cse_write_disjoint_nonaddressable_subregion_allows
-func.func @cse_write_disjoint_nonaddressable_subregion_allows() -> i32 {
+/// Check that a write to a resource disjoint from the read resource does NOT
+/// block CSE (sibling resources under different roots are disjoint).
+// CHECK-LABEL: @cse_write_disjoint_nonaddressable_subresource_allows
+func.func @cse_write_disjoint_nonaddressable_subresource_allows() -> i32 {
// CHECK-NEXT: %[[V:.*]] = "test.side_effect_op"()
%0 = "test.side_effect_op"() {effects = [{effect="read", test_nonaddressable_resource_a}]} : () -> i32
"test.side_effect_op"() {effects = [{effect="write", test_nonaddressable_resource_b}]} : () -> i32
@@ -640,8 +638,8 @@ func.func @cse_write_disjoint_nonaddressable_subregion_allows() -> i32 {
// -----
-/// Check that a write on an addressable custom resource still blocks CSE of
-/// reads on the default (addressable) resource (regression guard).
+/// Check that a write on a resource under DefaultResource still blocks CSE of
+/// reads on DefaultResource (regression guard).
// CHECK-LABEL: @cse_addressable_custom_resource_write_blocks
func.func @cse_addressable_custom_resource_write_blocks() -> i32 {
// CHECK-NEXT: %[[V0:.*]] = "test.op_with_memread"() : () -> i32
diff --git a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
index 0889ea0aace98..d11b563849ad7 100644
--- a/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
+++ b/mlir/test/lib/Dialect/Test/TestOpDefs.cpp
@@ -452,7 +452,7 @@ namespace {
struct TestResource : public SideEffects::Resource::Base<TestResource> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestResource)
- StringRef getName() final { return "<Test>"; }
+ StringRef getName() const final { return "<Test>"; }
};
} // namespace
diff --git a/mlir/test/lib/Dialect/Test/TestOps.h b/mlir/test/lib/Dialect/Test/TestOps.h
index 8b39699beb70a..b4cc2cd6cf569 100644
--- a/mlir/test/lib/Dialect/Test/TestOps.h
+++ b/mlir/test/lib/Dialect/Test/TestOps.h
@@ -51,57 +51,64 @@ class TestDialect;
// TestResource
//===----------------------------------------------------------------------===//
-/// A test resource for side effects.
-struct TestResource : public mlir::SideEffects::Resource::Base<TestResource> {
- llvm::StringRef getName() final { return "<Test>"; }
+/// A test resource for side effects (under DefaultResource).
+struct TestResource : public mlir::SideEffects::Resource::Base<
+ TestResource, mlir::SideEffects::DefaultResource> {
+ llvm::StringRef getName() const final { return "<Test>"; }
+ mlir::SideEffects::Resource *getParent() const override {
+ return mlir::SideEffects::DefaultResource::get();
+ }
};
-/// A test resource in NonAddressableMemory (disjoint from AddressableMemory).
+/// A test resource that is a root (disjoint from DefaultResource).
struct TestNonAddressableResource
: public mlir::SideEffects::Resource::Base<TestNonAddressableResource> {
- llvm::StringRef getName() final { return "<TestNonAddressable>"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
- }
+ llvm::StringRef getName() const final { return "<TestNonAddressable>"; }
+ bool isAddressable() const override { return false; }
};
-/// Two disjoint sub-regions under NonAddressableMemory for testing sibling
-/// disjointness within non-addressable memory.
-struct TestNonAddressableSubRegionA
- : public mlir::SideEffects::MemoryRegion::Base<
- TestNonAddressableSubRegionA> {
+/// Two disjoint sub-resources (roots) for testing sibling disjointness.
+struct TestNonAddressableSubResourceA
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableSubResourceA> {
+ TestNonAddressableSubResourceA() = default;
llvm::StringRef getName() const override {
- return "TestNonAddressableSubRegionA";
- }
- mlir::SideEffects::MemoryRegion *getParent() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
+ return "TestNonAddressableSubResourceA";
}
+ bool isAddressable() const override { return false; }
+
+protected:
+ TestNonAddressableSubResourceA(mlir::TypeID id) : Base(id) {}
};
-struct TestNonAddressableSubRegionB
- : public mlir::SideEffects::MemoryRegion::Base<
- TestNonAddressableSubRegionB> {
+struct TestNonAddressableSubResourceB
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableSubResourceB> {
+ TestNonAddressableSubResourceB() = default;
llvm::StringRef getName() const override {
- return "TestNonAddressableSubRegionB";
- }
- mlir::SideEffects::MemoryRegion *getParent() const override {
- return mlir::SideEffects::NonAddressableMemory::get();
+ return "TestNonAddressableSubResourceB";
}
+ bool isAddressable() const override { return false; }
+
+protected:
+ TestNonAddressableSubResourceB(mlir::TypeID id) : Base(id) {}
};
struct TestNonAddressableResourceA
- : public mlir::SideEffects::Resource::Base<TestNonAddressableResourceA> {
- llvm::StringRef getName() final { return "<TestNonAddressableA>"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return TestNonAddressableSubRegionA::get();
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableResourceA,
+ TestNonAddressableSubResourceA> {
+ llvm::StringRef getName() const final { return "<TestNonAddressableA>"; }
+ bool isAddressable() const override { return false; }
+ mlir::SideEffects::Resource *getParent() const override {
+ return TestNonAddressableSubResourceA::get();
}
};
struct TestNonAddressableResourceB
- : public mlir::SideEffects::Resource::Base<TestNonAddressableResourceB> {
- llvm::StringRef getName() final { return "<TestNonAddressableB>"; }
- mlir::SideEffects::MemoryRegion *getMemoryRegion() const override {
- return TestNonAddressableSubRegionB::get();
+ : public mlir::SideEffects::Resource::Base<TestNonAddressableResourceB,
+ TestNonAddressableSubResourceB> {
+ llvm::StringRef getName() const final { return "<TestNonAddressableB>"; }
+ bool isAddressable() const override { return false; }
+ mlir::SideEffects::Resource *getParent() const override {
+ return TestNonAddressableSubResourceB::get();
}
};
diff --git a/mlir/unittests/Interfaces/CMakeLists.txt b/mlir/unittests/Interfaces/CMakeLists.txt
index f40864d175629..7cb37323fcff2 100644
--- a/mlir/unittests/Interfaces/CMakeLists.txt
+++ b/mlir/unittests/Interfaces/CMakeLists.txt
@@ -2,6 +2,7 @@ add_mlir_unittest(MLIRInterfacesTests
ControlFlowInterfacesTest.cpp
DataLayoutInterfacesTest.cpp
InferIntRangeInterfaceTest.cpp
+ SideEffectInterfacesTest.cpp
InferTypeOpInterfaceTest.cpp
)
@@ -15,4 +16,5 @@ mlir_target_link_libraries(MLIRInterfacesTests
MLIRInferIntRangeInterface
MLIRInferTypeOpInterface
MLIRParser
+ MLIRSideEffectInterfaces
)
diff --git a/mlir/unittests/Interfaces/SideEffectInterfacesTest.cpp b/mlir/unittests/Interfaces/SideEffectInterfacesTest.cpp
new file mode 100644
index 0000000000000..2f7f7a889ccab
--- /dev/null
+++ b/mlir/unittests/Interfaces/SideEffectInterfacesTest.cpp
@@ -0,0 +1,123 @@
+//===- SideEffectInterfacesTest.cpp - Unit tests for Resource hierarchy ---===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Interfaces/SideEffectInterfaces.h"
+#include "mlir/Support/TypeID.h"
+#include "llvm/Support/Casting.h"
+#include <gtest/gtest.h>
+
+using namespace mlir;
+using namespace mlir::SideEffects;
+
+namespace {
+
+/// Custom resource hierarchy (root -> child -> grandchild) for testing.
+struct TestRootResource : public Resource::Base<TestRootResource> {
+ MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestRootResource)
+ TestRootResource() = default;
+ StringRef getName() const override { return "TestRoot"; }
+
+protected:
+ TestRootResource(TypeID id) : Base(id) {}
+};
+
+struct TestChildResource
+ : public Resource::Base<TestChildResource, TestRootResource> {
+ MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestChildResource)
+ TestChildResource() = default;
+ StringRef getName() const override { return "TestChild"; }
+ Resource *getParent() const override { return TestRootResource::get(); }
+
+protected:
+ TestChildResource(TypeID id) : Base(id) {}
+};
+
+struct TestGrandchildResource
+ : public Resource::Base<TestGrandchildResource, TestChildResource> {
+ MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestGrandchildResource)
+ StringRef getName() const override { return "TestGrandchild"; }
+ Resource *getParent() const override { return TestChildResource::get(); }
+};
+
+} // namespace
+
+TEST(SideEffectResourceTest, BuiltInHierarchyIsaCast) {
+ EXPECT_TRUE(isa<DefaultResource>(DefaultResource::get()));
+ EXPECT_TRUE(isa<DefaultResource>(AutomaticAllocationScopeResource::get()));
+ EXPECT_FALSE(isa<AutomaticAllocationScopeResource>(DefaultResource::get()));
+
+ // Each type has its own singleton; cast yields the child object as parent*
+ EXPECT_NE(cast<DefaultResource>(AutomaticAllocationScopeResource::get()),
+ nullptr);
+ EXPECT_TRUE(isa<DefaultResource>(
+ cast<DefaultResource>(AutomaticAllocationScopeResource::get())));
+
+ EXPECT_NE(dyn_cast<DefaultResource>(AutomaticAllocationScopeResource::get()),
+ nullptr);
+ EXPECT_EQ(dyn_cast<AutomaticAllocationScopeResource>(DefaultResource::get()),
+ nullptr);
+}
+
+TEST(SideEffectResourceTest, CustomHierarchyIsaCast) {
+ // Root and child
+ EXPECT_TRUE(isa<TestRootResource>(TestRootResource::get()));
+ EXPECT_TRUE(isa<TestRootResource>(TestChildResource::get()));
+ EXPECT_FALSE(isa<TestChildResource>(TestRootResource::get()));
+ EXPECT_NE(cast<TestRootResource>(TestChildResource::get()), nullptr);
+
+ // Grandchild isa root, child, and self
+ EXPECT_TRUE(isa<TestRootResource>(TestGrandchildResource::get()));
+ EXPECT_TRUE(isa<TestChildResource>(TestGrandchildResource::get()));
+ EXPECT_TRUE(isa<TestGrandchildResource>(TestGrandchildResource::get()));
+
+ // Root/child are not isa grandchild
+ EXPECT_FALSE(isa<TestGrandchildResource>(TestRootResource::get()));
+ EXPECT_FALSE(isa<TestGrandchildResource>(TestChildResource::get()));
+
+ // Cast grandchild to root and child (each type has its own singleton)
+ EXPECT_NE(cast<TestRootResource>(TestGrandchildResource::get()), nullptr);
+ EXPECT_NE(cast<TestChildResource>(TestGrandchildResource::get()), nullptr);
+
+ // dyn_cast
+ EXPECT_EQ(dyn_cast<TestGrandchildResource>(TestRootResource::get()), nullptr);
+ EXPECT_NE(dyn_cast<TestRootResource>(TestGrandchildResource::get()), nullptr);
+
+ // getParent chain
+ EXPECT_EQ(TestGrandchildResource::get()->getParent(),
+ TestChildResource::get());
+ EXPECT_EQ(TestChildResource::get()->getParent(), TestRootResource::get());
+ EXPECT_EQ(TestRootResource::get()->getParent(), nullptr);
+
+ // isSubresourceOf
+ EXPECT_TRUE(
+ TestGrandchildResource::get()->isSubresourceOf(TestRootResource::get()));
+ EXPECT_TRUE(
+ TestGrandchildResource::get()->isSubresourceOf(TestChildResource::get()));
+ EXPECT_FALSE(
+ TestRootResource::get()->isSubresourceOf(TestGrandchildResource::get()));
+
+ // Custom hierarchy disjoint from DefaultResource
+ EXPECT_TRUE(TestRootResource::get()->isDisjointFrom(DefaultResource::get()));
+ EXPECT_FALSE(isa<DefaultResource>(TestRootResource::get()));
+ EXPECT_FALSE(isa<DefaultResource>(TestChildResource::get()));
+ EXPECT_FALSE(isa<DefaultResource>(TestGrandchildResource::get()));
+ EXPECT_FALSE(isa<TestRootResource>(DefaultResource::get()));
+ EXPECT_EQ(dyn_cast<DefaultResource>(TestGrandchildResource::get()), nullptr);
+ EXPECT_EQ(dyn_cast<TestRootResource>(DefaultResource::get()), nullptr);
+}
+
+TEST(SideEffectResourceTest, DisjointnessAndGetParent) {
+ EXPECT_EQ(DefaultResource::get()->getParent(), nullptr);
+ EXPECT_EQ(AutomaticAllocationScopeResource::get()->getParent(),
+ DefaultResource::get());
+ EXPECT_TRUE(DefaultResource::get()->isDisjointFrom(TestRootResource::get()));
+ EXPECT_TRUE(
+ TestChildResource::get()->isSubresourceOf(TestRootResource::get()));
+ EXPECT_FALSE(
+ TestRootResource::get()->isSubresourceOf(TestChildResource::get()));
+}
More information about the flang-commits
mailing list