[llvm] [EarlyCSE] Remove void return restriction for call CSE (PR #145320)

Nikita Popov via llvm-commits llvm-commits at lists.llvm.org
Mon Jun 23 05:45:52 PDT 2025


https://github.com/nikic created https://github.com/llvm/llvm-project/pull/145320

For readonly/readnone calls returning void we can't CSE the return
value. However, making these participate in CSE is still useful,
because it allows DCE of calls that are not willreturn/nounwind
(something no other part of LLVM is capable of removing).

The more interesting use-case is CSE for writeonly calls (not
yet supported), but I figured this change makes sense independently.

There is no impact on compile-time.


>From 7177cd1a8e6e86d8e1cab7a1384c3a98eb4d23bc Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 23 Jun 2025 14:36:53 +0200
Subject: [PATCH 1/2] Add test with readonly/readnone void function

---
 llvm/test/Transforms/EarlyCSE/basic.ll | 28 ++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/llvm/test/Transforms/EarlyCSE/basic.ll b/llvm/test/Transforms/EarlyCSE/basic.ll
index 2c6b2a9613924..d7c6bd3fa9079 100644
--- a/llvm/test/Transforms/EarlyCSE/basic.ll
+++ b/llvm/test/Transforms/EarlyCSE/basic.ll
@@ -146,6 +146,30 @@ define i32 @test5(ptr%P) {
   ret i32 %Diff
 }
 
+declare void @void_func()
+
+define void @void_func_cse_readonly(ptr %P) {
+; CHECK-LABEL: @void_func_cse_readonly(
+; CHECK-NEXT:    call void @void_func(ptr [[P:%.*]]) #[[ATTR1:[0-9]+]]
+; CHECK-NEXT:    call void @void_func(ptr [[P]]) #[[ATTR1]]
+; CHECK-NEXT:    ret void
+;
+  call void @void_func(ptr %P) memory(read)
+  call void @void_func(ptr %P) memory(read)
+  ret void
+}
+
+define void @void_func_cse_readnone(ptr %P) {
+; CHECK-LABEL: @void_func_cse_readnone(
+; CHECK-NEXT:    call void @void_func(ptr [[P:%.*]]) #[[ATTR2:[0-9]+]]
+; CHECK-NEXT:    call void @void_func(ptr [[P]]) #[[ATTR2]]
+; CHECK-NEXT:    ret void
+;
+  call void @void_func(ptr %P) memory(none)
+  call void @void_func(ptr %P) memory(none)
+  ret void
+}
+
 !0 = !{!"branch_weights", i32 95}
 !1 = !{!"branch_weights", i32 95}
 
@@ -186,7 +210,7 @@ define void @test7(ptr%P) {
 ;; Readnone functions aren't invalidated by stores.
 define i32 @test8(ptr%P) {
 ; CHECK-LABEL: @test8(
-; CHECK-NEXT:    [[V1:%.*]] = call i32 @func(ptr [[P:%.*]]) #[[ATTR2:[0-9]+]]
+; CHECK-NEXT:    [[V1:%.*]] = call i32 @func(ptr [[P:%.*]]) #[[ATTR2]]
 ; CHECK-NEXT:    store i32 4, ptr [[P]], align 4
 ; CHECK-NEXT:    ret i32 0
 ;
@@ -202,7 +226,7 @@ define i32 @test8(ptr%P) {
 define i32 @test9(ptr%P) {
 ; CHECK-LABEL: @test9(
 ; CHECK-NEXT:    store i32 4, ptr [[P:%.*]], align 4
-; CHECK-NEXT:    [[V1:%.*]] = call i32 @func(ptr [[P]]) #[[ATTR1:[0-9]+]]
+; CHECK-NEXT:    [[V1:%.*]] = call i32 @func(ptr [[P]]) #[[ATTR1]]
 ; CHECK-NEXT:    store i32 5, ptr [[P]], align 4
 ; CHECK-NEXT:    ret i32 [[V1]]
 ;

>From ebcc8a0a15d662a4593e67ee8c9fe87e8bae1687 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Mon, 23 Jun 2025 10:01:38 +0200
Subject: [PATCH 2/2] [EarlyCSE] Remove void return restriction for call CSE

For readonly/readnone calls returns void we can't CSE the return
value. However, making these participate in CSE is still useful,
because we *can* CSE calls that are not willreturn/nounwind
(which otherwise cannot be DCEd).

The more interesting use-case is CSE for writeonly calls (not
yet supported), but I figured this change makes sense independently.

There is no impact on compile-time.
---
 llvm/lib/Transforms/Scalar/EarlyCSE.cpp       |  6 +---
 llvm/test/Transforms/EarlyCSE/basic.ll        |  2 --
 llvm/test/Transforms/EarlyCSE/flags.ll        | 26 +++++++-------
 .../Transforms/EarlyCSE/noalias-addrspace.ll  | 34 +++++++++----------
 4 files changed, 31 insertions(+), 37 deletions(-)

diff --git a/llvm/lib/Transforms/Scalar/EarlyCSE.cpp b/llvm/lib/Transforms/Scalar/EarlyCSE.cpp
index e1e283f171d38..381de60fcface 100644
--- a/llvm/lib/Transforms/Scalar/EarlyCSE.cpp
+++ b/llvm/lib/Transforms/Scalar/EarlyCSE.cpp
@@ -133,7 +133,7 @@ struct SimpleValue {
         }
         }
       }
-      return CI->doesNotAccessMemory() && !CI->getType()->isVoidTy() &&
+      return CI->doesNotAccessMemory() &&
              // 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
@@ -492,10 +492,6 @@ struct CallValue {
   }
 
   static bool canHandle(Instruction *Inst) {
-    // Don't value number anything that returns void.
-    if (Inst->getType()->isVoidTy())
-      return false;
-
     CallInst *CI = dyn_cast<CallInst>(Inst);
     if (!CI || !CI->onlyReadsMemory() ||
         // FIXME: Currently the calls which may access the thread id may
diff --git a/llvm/test/Transforms/EarlyCSE/basic.ll b/llvm/test/Transforms/EarlyCSE/basic.ll
index d7c6bd3fa9079..f877235ed9787 100644
--- a/llvm/test/Transforms/EarlyCSE/basic.ll
+++ b/llvm/test/Transforms/EarlyCSE/basic.ll
@@ -151,7 +151,6 @@ declare void @void_func()
 define void @void_func_cse_readonly(ptr %P) {
 ; CHECK-LABEL: @void_func_cse_readonly(
 ; CHECK-NEXT:    call void @void_func(ptr [[P:%.*]]) #[[ATTR1:[0-9]+]]
-; CHECK-NEXT:    call void @void_func(ptr [[P]]) #[[ATTR1]]
 ; CHECK-NEXT:    ret void
 ;
   call void @void_func(ptr %P) memory(read)
@@ -162,7 +161,6 @@ define void @void_func_cse_readonly(ptr %P) {
 define void @void_func_cse_readnone(ptr %P) {
 ; CHECK-LABEL: @void_func_cse_readnone(
 ; CHECK-NEXT:    call void @void_func(ptr [[P:%.*]]) #[[ATTR2:[0-9]+]]
-; CHECK-NEXT:    call void @void_func(ptr [[P]]) #[[ATTR2]]
 ; CHECK-NEXT:    ret void
 ;
   call void @void_func(ptr %P) memory(none)
diff --git a/llvm/test/Transforms/EarlyCSE/flags.ll b/llvm/test/Transforms/EarlyCSE/flags.ll
index 78b3818b211da..dcaaacbac639f 100644
--- a/llvm/test/Transforms/EarlyCSE/flags.ll
+++ b/llvm/test/Transforms/EarlyCSE/flags.ll
@@ -3,7 +3,7 @@
 ; RUN: opt -passes='early-cse<memssa>' -S < %s | FileCheck %s
 
 declare void @use(i1)
-declare void @use.ptr(ptr) memory(read)
+declare void @use.ptr(i32, ptr) memory(read)
 
 define void @test1(float %x, float %y) {
 ; CHECK-LABEL: @test1(
@@ -52,42 +52,42 @@ define void @test_inbounds_program_not_ub_if_first_gep_poison(ptr %ptr, i64 %n)
 define void @load_both_nonnull(ptr %p) {
 ; CHECK-LABEL: @load_both_nonnull(
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P:%.*]], align 8, !nonnull [[META0:![0-9]+]]
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p, !nonnull !{}
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 
 define void @load_first_nonnull(ptr %p) {
 ; CHECK-LABEL: @load_first_nonnull(
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P:%.*]], align 8
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 
 define void @load_first_nonnull_noundef(ptr %p) {
 ; CHECK-LABEL: @load_first_nonnull_noundef(
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P:%.*]], align 8, !nonnull [[META0]], !noundef [[META0]]
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}, !noundef !{}
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 
diff --git a/llvm/test/Transforms/EarlyCSE/noalias-addrspace.ll b/llvm/test/Transforms/EarlyCSE/noalias-addrspace.ll
index 0a001b55f684c..2d67186531481 100644
--- a/llvm/test/Transforms/EarlyCSE/noalias-addrspace.ll
+++ b/llvm/test/Transforms/EarlyCSE/noalias-addrspace.ll
@@ -2,20 +2,20 @@
 ; RUN: opt -passes='early-cse<memssa>' -S < %s | FileCheck %s
 
 declare void @use(i1)
-declare void @use.ptr(ptr) memory(read)
+declare void @use.ptr(i32, ptr) memory(read)
 
 define void @load_first_noalias_addrspace(ptr %p) {
 ; CHECK-LABEL: define void @load_first_noalias_addrspace(
 ; CHECK-SAME: ptr [[P:%.*]]) {
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P]], align 8, !nonnull [[META0:![0-9]+]], !noundef [[META0]], !noalias.addrspace [[META1:![0-9]+]]
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}, !noundef !{}, !noalias.addrspace !0
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 
@@ -23,14 +23,14 @@ define void @load_both_same_noalias_addrspace(ptr %p) {
 ; CHECK-LABEL: define void @load_both_same_noalias_addrspace(
 ; CHECK-SAME: ptr [[P:%.*]]) {
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P]], align 8, !nonnull [[META0]], !noundef [[META0]], !noalias.addrspace [[META1]]
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}, !noundef !{}, !noalias.addrspace !0
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p, !noalias.addrspace !0
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 
@@ -38,14 +38,14 @@ define void @load_both_disjoint_noalias_addrspace(ptr %p) {
 ; CHECK-LABEL: define void @load_both_disjoint_noalias_addrspace(
 ; CHECK-SAME: ptr [[P:%.*]]) {
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P]], align 8, !nonnull [[META0]], !noundef [[META0]], !noalias.addrspace [[META1]]
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}, !noundef !{}, !noalias.addrspace !0
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p, !noalias.addrspace !1
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 
@@ -53,14 +53,14 @@ define void @load_both_overlap_noalias_addrspace(ptr %p) {
 ; CHECK-LABEL: define void @load_both_overlap_noalias_addrspace(
 ; CHECK-SAME: ptr [[P:%.*]]) {
 ; CHECK-NEXT:    [[V1:%.*]] = load ptr, ptr [[P]], align 8, !nonnull [[META0]], !noundef [[META0]], !noalias.addrspace [[META1]]
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
-; CHECK-NEXT:    call void @use.ptr(ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 0, ptr [[V1]])
+; CHECK-NEXT:    call void @use.ptr(i32 1, ptr [[V1]])
 ; CHECK-NEXT:    ret void
 ;
   %v1 = load ptr, ptr %p, !nonnull !{}, !noundef !{}, !noalias.addrspace !0
-  call void @use.ptr(ptr %v1)
+  call void @use.ptr(i32 0, ptr %v1)
   %v2 = load ptr, ptr %p, !noalias.addrspace !2
-  call void @use.ptr(ptr %v2)
+  call void @use.ptr(i32 1, ptr %v2)
   ret void
 }
 



More information about the llvm-commits mailing list