[llvm] de6b8cd - [EarlyCSE] Add support for writeonly call CSE (#145474)
via llvm-commits
llvm-commits at lists.llvm.org
Mon Jun 30 02:56:36 PDT 2025
Author: Nikita Popov
Date: 2025-06-30T11:56:32+02:00
New Revision: de6b8cdc41123b71c2306384fd177a18a504a187
URL: https://github.com/llvm/llvm-project/commit/de6b8cdc41123b71c2306384fd177a18a504a187
DIFF: https://github.com/llvm/llvm-project/commit/de6b8cdc41123b71c2306384fd177a18a504a187.diff
LOG: [EarlyCSE] Add support for writeonly call CSE (#145474)
Add support for CSE of writeonly calls, similar to the existing support
for readonly calls.
Added:
Modified:
clang/test/CodeGenCXX/auto-var-init.cpp
llvm/lib/Transforms/Scalar/EarlyCSE.cpp
llvm/test/Transforms/EarlyCSE/writeonly.ll
Removed:
################################################################################
diff --git a/clang/test/CodeGenCXX/auto-var-init.cpp b/clang/test/CodeGenCXX/auto-var-init.cpp
index 94386e44573b5..67bc5d417bce9 100644
--- a/clang/test/CodeGenCXX/auto-var-init.cpp
+++ b/clang/test/CodeGenCXX/auto-var-init.cpp
@@ -1342,6 +1342,7 @@ TEST_UNINIT(base, base);
// ZERO-O1-NOT: !annotation
TEST_BRACES(base, base);
+// ZERO-LABEL: @test_base_braces()
// CHECK-LABEL: @test_base_braces()
// CHECK: %braces = alloca %struct.base, align [[ALIGN:[0-9]*]]
// CHECK-NEXT: call void @llvm.memset{{.*}}(ptr align [[ALIGN]] %{{.*}}, i8 0, i64 8, i1 false)
diff --git a/llvm/lib/Transforms/Scalar/EarlyCSE.cpp b/llvm/lib/Transforms/Scalar/EarlyCSE.cpp
index 381de60fcface..b6cb987c0423f 100644
--- a/llvm/lib/Transforms/Scalar/EarlyCSE.cpp
+++ b/llvm/lib/Transforms/Scalar/EarlyCSE.cpp
@@ -493,7 +493,7 @@ struct CallValue {
static bool canHandle(Instruction *Inst) {
CallInst *CI = dyn_cast<CallInst>(Inst);
- if (!CI || !CI->onlyReadsMemory() ||
+ if (!CI || (!CI->onlyReadsMemory() && !CI->onlyWritesMemory()) ||
// FIXME: Currently the calls which may access the thread id may
// be considered as not accessing the memory. But this is
// problematic for coroutines, since coroutines may resume in a
@@ -1626,14 +1626,19 @@ bool EarlyCSE::processNode(DomTreeNode *Node) {
!(MemInst.isValid() && !MemInst.mayReadFromMemory()))
LastStore = nullptr;
- // If this is a read-only call, process it.
- if (CallValue::canHandle(&Inst)) {
+ // If this is a read-only or write-only call, process it. Skip store
+ // MemInsts, as they will be more precisely handled later on. Also skip
+ // memsets, as DSE may be able to optimize them better by removing the
+ // earlier rather than later store.
+ if (CallValue::canHandle(&Inst) &&
+ (!MemInst.isValid() || !MemInst.isStore()) && !isa<MemSetInst>(&Inst)) {
// If we have an available version of this call, and if it is the right
// generation, replace this instruction.
std::pair<Instruction *, unsigned> InVal = AvailableCalls.lookup(&Inst);
if (InVal.first != nullptr &&
isSameMemGeneration(InVal.second, CurrentGeneration, InVal.first,
- &Inst)) {
+ &Inst) &&
+ InVal.first->mayReadFromMemory() == Inst.mayReadFromMemory()) {
LLVM_DEBUG(dbgs() << "EarlyCSE CSE CALL: " << Inst
<< " to: " << *InVal.first << '\n');
if (!DebugCounter::shouldExecute(CSECounter)) {
@@ -1651,6 +1656,11 @@ bool EarlyCSE::processNode(DomTreeNode *Node) {
continue;
}
+ // Increase memory generation for writes. Do this before inserting
+ // the call, so it has the generation after the write occurred.
+ if (Inst.mayWriteToMemory())
+ ++CurrentGeneration;
+
// Otherwise, remember that we have this instruction.
AvailableCalls.insert(&Inst, std::make_pair(&Inst, CurrentGeneration));
continue;
diff --git a/llvm/test/Transforms/EarlyCSE/writeonly.ll b/llvm/test/Transforms/EarlyCSE/writeonly.ll
index 0bfffa3c825a3..c09b913f9ff2b 100644
--- a/llvm/test/Transforms/EarlyCSE/writeonly.ll
+++ b/llvm/test/Transforms/EarlyCSE/writeonly.ll
@@ -1,5 +1,6 @@
; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
-; RUN: opt -S -passes=early-cse -earlycse-debug-hash < %s | FileCheck %s
+; RUN: opt -S -passes=early-cse -earlycse-debug-hash < %s | FileCheck %s --check-prefixes=CHECK,NO-MSSA
+; RUN: opt -S -passes='early-cse<memssa>' < %s | FileCheck %s --check-prefixes=CHECK,MSSA
@var = global i32 undef
declare void @foo() nounwind
@@ -15,3 +16,148 @@ define void @test() {
store i32 2, ptr @var
ret void
}
+
+declare void @writeonly_void() memory(write)
+
+; Can CSE writeonly calls, including non-nounwind/willreturn.
+define void @writeonly_cse() {
+; CHECK-LABEL: @writeonly_cse(
+; CHECK-NEXT: call void @writeonly_void()
+; CHECK-NEXT: ret void
+;
+ call void @writeonly_void()
+ call void @writeonly_void()
+ ret void
+}
+
+; Can CSE, loads do not matter.
+define i32 @writeonly_cse_intervening_load(ptr %p) {
+; CHECK-LABEL: @writeonly_cse_intervening_load(
+; CHECK-NEXT: call void @writeonly_void()
+; CHECK-NEXT: [[V:%.*]] = load i32, ptr [[P:%.*]], align 4
+; CHECK-NEXT: ret i32 [[V]]
+;
+ call void @writeonly_void()
+ %v = load i32, ptr %p
+ call void @writeonly_void()
+ ret i32 %v
+}
+
+; Cannot CSE, the store may be to the same memory.
+define void @writeonly_cse_intervening_store(ptr %p) {
+; CHECK-LABEL: @writeonly_cse_intervening_store(
+; CHECK-NEXT: call void @writeonly_void()
+; CHECK-NEXT: store i32 0, ptr [[P:%.*]], align 4
+; CHECK-NEXT: call void @writeonly_void()
+; CHECK-NEXT: ret void
+;
+ call void @writeonly_void()
+ store i32 0, ptr %p
+ call void @writeonly_void()
+ ret void
+}
+
+; Can CSE, the store does not alias the writeonly call.
+define void @writeonly_cse_intervening_noalias_store(ptr noalias %p) {
+; NO-MSSA-LABEL: @writeonly_cse_intervening_noalias_store(
+; NO-MSSA-NEXT: call void @writeonly_void()
+; NO-MSSA-NEXT: store i32 0, ptr [[P:%.*]], align 4
+; NO-MSSA-NEXT: call void @writeonly_void()
+; NO-MSSA-NEXT: ret void
+;
+; MSSA-LABEL: @writeonly_cse_intervening_noalias_store(
+; MSSA-NEXT: call void @writeonly_void()
+; MSSA-NEXT: store i32 0, ptr [[P:%.*]], align 4
+; MSSA-NEXT: ret void
+;
+ call void @writeonly_void()
+ store i32 0, ptr %p
+ call void @writeonly_void()
+ ret void
+}
+
+; Cannot CSE loads across writeonly call.
+define i32 @load_cse_across_writeonly(ptr %p) {
+; CHECK-LABEL: @load_cse_across_writeonly(
+; CHECK-NEXT: [[V1:%.*]] = load i32, ptr [[P:%.*]], align 4
+; CHECK-NEXT: call void @writeonly_void()
+; CHECK-NEXT: [[V2:%.*]] = load i32, ptr [[P]], align 4
+; CHECK-NEXT: [[RES:%.*]] = sub i32 [[V1]], [[V2]]
+; CHECK-NEXT: ret i32 [[RES]]
+;
+ %v1 = load i32, ptr %p
+ call void @writeonly_void()
+ %v2 = load i32, ptr %p
+ %res = sub i32 %v1, %v2
+ ret i32 %res
+}
+
+; Can CSE loads across eliminated writeonly call.
+define i32 @load_cse_across_csed_writeonly(ptr %p) {
+; CHECK-LABEL: @load_cse_across_csed_writeonly(
+; CHECK-NEXT: call void @writeonly_void()
+; CHECK-NEXT: [[V2:%.*]] = load i32, ptr [[P:%.*]], align 4
+; CHECK-NEXT: ret i32 0
+;
+ call void @writeonly_void()
+ %v1 = load i32, ptr %p
+ call void @writeonly_void()
+ %v2 = load i32, ptr %p
+ %res = sub i32 %v1, %v2
+ ret i32 %res
+}
+
+declare i32 @writeonly(ptr %p) memory(write)
+
+; Can CSE writeonly calls with arg and return.
+define i32 @writeonly_ret_cse(ptr %p) {
+; CHECK-LABEL: @writeonly_ret_cse(
+; CHECK-NEXT: [[V2:%.*]] = call i32 @writeonly(ptr [[P:%.*]])
+; CHECK-NEXT: ret i32 0
+;
+ %v1 = call i32 @writeonly(ptr %p)
+ %v2 = call i32 @writeonly(ptr %p)
+ %res = sub i32 %v1, %v2
+ ret i32 %res
+}
+
+; Cannot CSE writeonly calls with
diff erent arguments.
+define i32 @writeonly_
diff erent_args(ptr %p1, ptr %p2) {
+; CHECK-LABEL: @writeonly_
diff erent_args(
+; CHECK-NEXT: [[V1:%.*]] = call i32 @writeonly(ptr [[P1:%.*]])
+; CHECK-NEXT: [[V2:%.*]] = call i32 @writeonly(ptr [[P2:%.*]])
+; CHECK-NEXT: [[RES:%.*]] = sub i32 [[V1]], [[V2]]
+; CHECK-NEXT: ret i32 [[RES]]
+;
+ %v1 = call i32 @writeonly(ptr %p1)
+ %v2 = call i32 @writeonly(ptr %p2)
+ %res = sub i32 %v1, %v2
+ ret i32 %res
+}
+
+declare void @callee()
+
+; These are weird cases where the same call is both readonly and writeonly
+; based on call-site attributes. I believe this implies that both calls are
+; actually readnone and safe to CSE, but leave them alone to be conservative.
+define void @readonly_and_writeonly() {
+; CHECK-LABEL: @readonly_and_writeonly(
+; CHECK-NEXT: call void @callee() #[[ATTR2:[0-9]+]]
+; CHECK-NEXT: call void @callee() #[[ATTR1]]
+; CHECK-NEXT: ret void
+;
+ call void @callee() memory(read)
+ call void @callee() memory(write)
+ ret void
+}
+
+define void @writeonly_and_readonly() {
+; CHECK-LABEL: @writeonly_and_readonly(
+; CHECK-NEXT: call void @callee() #[[ATTR1]]
+; CHECK-NEXT: call void @callee() #[[ATTR2]]
+; CHECK-NEXT: ret void
+;
+ call void @callee() memory(write)
+ call void @callee() memory(read)
+ ret void
+}
More information about the llvm-commits
mailing list