[llvm] [SimplifyCFG] Drop UB-implying metadata when hoisting past side effects (PR #226746)

via llvm-commits llvm-commits at lists.llvm.org
Sat Sep 26 20:03:11 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Mian Miftah (mmiftahx)

<details>
<summary>Changes</summary>

`hoistCommonCodeFromSuccessors()` can hoist identical instructions past
non-identical ones that it skips, but the hoisted instruction keeps its
attributes and metadata.

If a skipped instruction may write memory, metadata that only holds at
the hoisted instruction's old position may not hold before the write.
For example, an `inttoptr` with `!dereferenceable` is hoisted above the call
that maps the memory. Drop `!dereferenceable`, `!dereferenceable_or_null`
and `!nofreeobj` in that case. Nothing is speculated then, so `noundef` is
kept.

If a skipped instruction may throw or not return, the hoisted
instruction is speculated, so drop UB-implying attributes and
metadata, as SimplifyCFG already does when it speculates.

---
Full diff: https://github.com/llvm/llvm-project/pull/226746.diff


2 Files Affected:

- (modified) llvm/lib/Transforms/Utils/SimplifyCFG.cpp (+14) 
- (modified) llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll (+135-1) 


``````````diff
diff --git a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
index 7134ab2a7f40c1..dd0fca91f28630 100644
--- a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
+++ b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
@@ -2053,6 +2053,20 @@ bool SimplifyCFGOpt::hoistCommonCodeFromSuccessors(Instruction *TI,
         I1->applyMergedLocation(I1->getDebugLoc(), I2->getDebugLoc());
         I2->eraseFromParent();
       }
+      // I1 now executes before the instructions we skipped.
+      unsigned SkippedFlags = 0;
+      for (const SuccIterPair &P : SuccIterPairs)
+        SkippedFlags |= P.second;
+      if (SkippedFlags & SkipImplicitControlFlow) {
+        // One of them may throw or not return, so I1 is speculated.
+        I1->dropUBImplyingAttrsAndMetadata();
+      } else if (SkippedFlags & SkipSideEffect) {
+        // One of them may write memory, for example allocate or free it, so
+        // metadata that only holds at I1's old position may not hold here.
+        I1->setMetadata(LLVMContext::MD_dereferenceable, nullptr);
+        I1->setMetadata(LLVMContext::MD_dereferenceable_or_null, nullptr);
+        I1->setMetadata(LLVMContext::MD_nofreeobj, nullptr);
+      }
       if (!Changed)
         NumHoistCommonCode += SuccIterPairs.size();
       Changed = true;
diff --git a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
index 3e6f31176315bf..cfb67e5a2c73a0 100644
--- a/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
+++ b/llvm/test/Transforms/SimplifyCFG/hoist-common-skip.ll
@@ -1,5 +1,5 @@
 ; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
-; RUN: opt -S --passes='simplifycfg<hoist-common-insts>' %s | FileCheck %s
+; RUN: opt -S --passes='simplifycfg<hoist-common-insts>' %s | FileCheck %s --implicit-check-not='!dereferenceable'
 
 ;; Check that the two loads are hoisted to the common predecessor, skipping
 ;; over the add/sub instructions.
@@ -1150,3 +1150,137 @@ j:
   %p = phi ptr [ %call, %t ], [ %call2, %e ]
   ret ptr %p
 }
+
+declare void @map(i64) nounwind willreturn
+
+; The skipped calls may allocate the memory at %i, so !dereferenceable and
+; !dereferenceable_or_null may not hold before them.
+define ptr @hoist_deref_past_call(i1 %c, i64 %i) {
+; CHECK-LABEL: @hoist_deref_past_call(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr
+; CHECK-NEXT:    br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK:       if:
+; CHECK-NEXT:    call void @map(i64 [[I]])
+; CHECK-NEXT:    br label [[END:%.*]]
+; CHECK:       else:
+; CHECK-NEXT:    call void @map(i64 0)
+; CHECK-NEXT:    br label [[END]]
+; CHECK:       end:
+; CHECK-NEXT:    ret ptr [[P]]
+;
+entry:
+  br i1 %c, label %if, label %else
+if:
+  call void @map(i64 %i)
+  %p = inttoptr i64 %i to ptr, !dereferenceable !0, !dereferenceable_or_null !0
+  br label %end
+else:
+  call void @map(i64 0)
+  %q = inttoptr i64 %i to ptr, !dereferenceable !0, !dereferenceable_or_null !0
+  br label %end
+end:
+  %r = phi ptr [ %p, %if ], [ %q, %else ]
+  ret ptr %r
+}
+
+; Skipped loads do not affect !dereferenceable.
+define ptr @hoist_deref_past_load(i1 %c, i64 %i, ptr %x, ptr %y, ptr %out) {
+; CHECK-LABEL: @hoist_deref_past_load(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[P:%.*]] = inttoptr i64 [[I:%.*]] to ptr, !dereferenceable [[META0:![0-9]+]]
+; CHECK-NEXT:    br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK:       if:
+; CHECK-NEXT:    [[A:%.*]] = load i32, ptr [[X:%.*]], align 4
+; CHECK-NEXT:    br label [[END:%.*]]
+; CHECK:       else:
+; CHECK-NEXT:    [[B:%.*]] = load i32, ptr [[Y:%.*]], align 4
+; CHECK-NEXT:    br label [[END]]
+; CHECK:       end:
+; CHECK-NEXT:    [[V:%.*]] = phi i32 [ [[A]], [[IF]] ], [ [[B]], [[ELSE]] ]
+; CHECK-NEXT:    [[R:%.*]] = phi ptr [ [[P]], [[IF]] ], [ [[P]], [[ELSE]] ]
+; CHECK-NEXT:    store i32 [[V]], ptr [[OUT:%.*]], align 4
+; CHECK-NEXT:    ret ptr [[R]]
+;
+entry:
+  br i1 %c, label %if, label %else
+if:
+  %a = load i32, ptr %x
+  %p = inttoptr i64 %i to ptr, !dereferenceable !0
+  br label %end
+else:
+  %b = load i32, ptr %y
+  %q = inttoptr i64 %i to ptr, !dereferenceable !0
+  br label %end
+end:
+  %v = phi i32 [ %a, %if ], [ %b, %else ]
+  %r = phi ptr [ %p, %if ], [ %q, %else ]
+  store i32 %v, ptr %out
+  ret ptr %r
+}
+
+declare i32 @pure(i32) memory(none) nounwind willreturn
+declare i32 @pure_speculatable(i32) memory(none) nounwind willreturn speculatable
+
+; The skipped calls may write memory, but noundef does not depend on it.
+define i32 @hoist_noundef_past_call(i1 %c, i64 %i, i32 %x) {
+; CHECK-LABEL: @hoist_noundef_past_call(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[A:%.*]] = call noundef i32 @pure(i32 noundef [[X:%.*]])
+; CHECK-NEXT:    br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK:       if:
+; CHECK-NEXT:    call void @map(i64 [[I:%.*]])
+; CHECK-NEXT:    br label [[END:%.*]]
+; CHECK:       else:
+; CHECK-NEXT:    call void @map(i64 0)
+; CHECK-NEXT:    br label [[END]]
+; CHECK:       end:
+; CHECK-NEXT:    ret i32 [[A]]
+;
+entry:
+  br i1 %c, label %if, label %else
+if:
+  call void @map(i64 %i)
+  %a = call noundef i32 @pure(i32 noundef %x)
+  br label %end
+else:
+  call void @map(i64 0)
+  %b = call noundef i32 @pure(i32 noundef %x)
+  br label %end
+end:
+  %r = phi i32 [ %a, %if ], [ %b, %else ]
+  ret i32 %r
+}
+
+; The skipped calls may throw, so the hoisted call is speculated and noundef
+; may not hold.
+define i32 @hoist_noundef_past_throwing_call(i1 %c, i32 %x) {
+; CHECK-LABEL: @hoist_noundef_past_throwing_call(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[A:%.*]] = call i32 @pure_speculatable(i32 [[X:%.*]])
+; CHECK-NEXT:    br i1 [[C:%.*]], label [[IF:%.*]], label [[ELSE:%.*]]
+; CHECK:       if:
+; CHECK-NEXT:    call void @side_effects0()
+; CHECK-NEXT:    br label [[END:%.*]]
+; CHECK:       else:
+; CHECK-NEXT:    call void @side_effects1()
+; CHECK-NEXT:    br label [[END]]
+; CHECK:       end:
+; CHECK-NEXT:    ret i32 [[A]]
+;
+entry:
+  br i1 %c, label %if, label %else
+if:
+  call void @side_effects0()
+  %a = call noundef i32 @pure_speculatable(i32 noundef %x)
+  br label %end
+else:
+  call void @side_effects1()
+  %b = call noundef i32 @pure_speculatable(i32 noundef %x)
+  br label %end
+end:
+  %r = phi i32 [ %a, %if ], [ %b, %else ]
+  ret i32 %r
+}
+
+!0 = !{i64 4}

``````````

</details>


https://github.com/llvm/llvm-project/pull/226746


More information about the llvm-commits mailing list