[llvm] [InstCombine][CVP] Fold length-one memset with variable fill (PR #213240)

via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 31 03:38:12 PDT 2026


https://github.com/hsnaveen2u created https://github.com/llvm/llvm-project/pull/213240

This series handles memset calls whose length is known to be in the
unsigned range [0, 1].

The first commit allows InstCombine to replace a length-one memset with
a byte store when the fill value is nonconstant. It preserves volatile
and unordered atomic semantics.

The second commit uses LazyValueInfo in CVP to guard a memset whose
length is known to be in [0, 1], specializing the executed path to a
constant length of one. A following InstCombine pass can then replace
the memset with a byte store.

The [0, 2] case remains unchanged.

Testing:
- Targeted InstCombine test
- Targeted CorrelatedValuePropagation test
- check-llvm-transforms
- check-llvm regression, compared with the unpatched baseline

Fixes #213027.

>From af6e833c9c6de2a527a75e0cbcc2d7fb689d6d9e Mon Sep 17 00:00:00 2001
From: Naveen <naveen.siddegowda at oss.qualcomm.com>
Date: Fri, 31 Jul 2026 03:01:29 -0700
Subject: [PATCH 1/2] [InstCombine] Fold length-one memset with variable fill

A one-byte memset does not require replicating the fill byte into a
wider integer value. Allow a nonconstant i8 fill value to be stored
directly when the memset length is one.

Keep the existing constant-fill handling for lengths 1, 2, 4 and 8.
Preserve volatility and unordered atomic ordering on the generated
store.

Allow volatile AnyMemSetInst operations to reach SimplifyAnyMemSet
while continuing to block other volatile memory-intrinsic
transformations.

This is the InstCombine prerequisite for #213027.

Signed-off-by: Naveen <naveen.siddegowda at oss.qualcomm.com>
---
 .../InstCombine/InstCombineCalls.cpp          | 49 +++++++++++----
 .../InstCombine/memset-variable-fill.ll       | 59 +++++++++++++++++++
 2 files changed, 97 insertions(+), 11 deletions(-)
 create mode 100644 llvm/test/Transforms/InstCombine/memset-variable-fill.ll

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
index 5ee5009bd0262..5c5522cadd0b9 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
@@ -221,6 +221,24 @@ Instruction *InstCombinerImpl::SimplifyAnyMemTransfer(AnyMemTransferInst *MI) {
 }
 
 Instruction *InstCombinerImpl::SimplifyAnyMemSet(AnyMemSetInst *MI) {
+  ConstantInt *LenC = dyn_cast<ConstantInt>(MI->getLength());
+  Value *Fill = MI->getValue();
+
+  // Keep volatile memset scalarization limited to the single-byte case
+  // where the replacement is exactly one volatile byte store.
+  if (MI->isVolatile()) {
+    if (!LenC || !LenC->isOne() || !Fill->getType()->isIntegerTy(8))
+      return nullptr;
+
+    StoreInst *S = Builder.CreateStore(Fill, MI->getDest(), true);
+    S->copyMetadata(*MI, LLVMContext::MD_DIAssignID);
+    S->setAlignment(MI->getDestAlign().valueOrOne());
+
+    // Set the size of the copy to 0 and will be deleted on the next iteration.
+    MI->setLength((uint64_t)0);
+    return MI;
+  }
+
   const Align KnownAlignment =
       getKnownAlignment(MI->getDest(), DL, MI, &AC, &DT);
   MaybeAlign MemSetAlign = MI->getDestAlign();
@@ -247,10 +265,8 @@ Instruction *InstCombinerImpl::SimplifyAnyMemSet(AnyMemSetInst *MI) {
     return MI;
   }
 
-  // Extract the length and alignment and fill if they are constant.
-  ConstantInt *LenC = dyn_cast<ConstantInt>(MI->getLength());
-  ConstantInt *FillC = dyn_cast<ConstantInt>(MI->getValue());
-  if (!LenC || !FillC || !FillC->getType()->isIntegerTy(8))
+  // Extract the length and validate the fill type.
+  if (!LenC || !Fill->getType()->isIntegerTy(8))
     return nullptr;
   const uint64_t Len = LenC->getLimitedValue();
   assert(Len && "0-sized memory setting should be removed already.");
@@ -267,14 +283,22 @@ Instruction *InstCombinerImpl::SimplifyAnyMemSet(AnyMemSetInst *MI) {
   if (Len <= 8 && isPowerOf2_32((uint32_t)Len)) {
     Value *Dest = MI->getDest();
 
-    // Extract the fill value and store.
-    Constant *FillVal = ConstantInt::get(
-        MI->getContext(), APInt::getSplat(Len * 8, FillC->getValue()));
+    // Extract the fill value and store.  A one-byte memset does not need
+    // replication so a nonconstant i8 fill can be stored directly.
+    Value *FillVal;
+    if (auto *FillC = dyn_cast<ConstantInt>(Fill))
+      FillVal = ConstantInt::get(MI->getContext(),
+                                 APInt::getSplat(Len * 8, FillC->getValue()));
+    else if (Len == 1)
+      FillVal = Fill;
+    else
+      return nullptr;
+
     StoreInst *S = Builder.CreateStore(FillVal, Dest, MI->isVolatile());
     S->copyMetadata(*MI, LLVMContext::MD_DIAssignID);
     for (DbgVariableRecord *DbgAssign : at::getDVRAssignmentMarkers(S)) {
-      if (llvm::is_contained(DbgAssign->location_ops(), FillC))
-        DbgAssign->replaceVariableLocationOp(FillC, FillVal);
+      if (llvm::is_contained(DbgAssign->location_ops(), Fill))
+        DbgAssign->replaceVariableLocationOp(Fill, FillVal);
     }
 
     S->setAlignment(Alignment);
@@ -2024,8 +2048,9 @@ Instruction *InstCombinerImpl::visitCallInst(CallInst &CI) {
       }
     }
 
-    // No other transformations apply to volatile transfers.
-    if (MI->isVolatile())
+    // Apart from memset-to-store scalarization below no other transformations
+    // apply to volatile transfers.
+    if (MI->isVolatile() && !isa<AnyMemSetInst>(MI))
       return nullptr;
 
     if (AnyMemTransferInst *MTI = dyn_cast<AnyMemTransferInst>(MI)) {
@@ -2050,6 +2075,8 @@ Instruction *InstCombinerImpl::visitCallInst(CallInst &CI) {
     } else if (auto *MSI = dyn_cast<AnyMemSetInst>(MI)) {
       if (Instruction *I = SimplifyAnyMemSet(MSI))
         return I;
+      if (MI->isVolatile())
+        return nullptr;
     }
 
     // If src/dest is null, this memory intrinsic must be a noop.
diff --git a/llvm/test/Transforms/InstCombine/memset-variable-fill.ll b/llvm/test/Transforms/InstCombine/memset-variable-fill.ll
new file mode 100644
index 0000000000000..b5547071a05d5
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/memset-variable-fill.ll
@@ -0,0 +1,59 @@
+; RUN: opt -passes=instcombine -S < %s | FileCheck %s
+
+declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
+declare void @llvm.memset.p1.i64(ptr addrspace(1) nocapture writeonly, i8, i64, i1 immarg)
+declare void @llvm.memset.element.unordered.atomic.p0.i64(ptr nocapture writeonly, i8, i64, i32 immarg)
+
+define void @variable_fill_len1(ptr %dst, i8 %value) {
+; CHECK-LABEL: define void @variable_fill_len1(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]]) {
+; CHECK-NEXT:    store i8 [[VALUE]], ptr [[DST]], align 1
+; CHECK-NEXT:    ret void
+  call void @llvm.memset.p0.i64(ptr align 1 %dst, i8 %value, i64 1, i1 false)
+  ret void
+}
+
+define void @variable_fill_len1_volatile(ptr %dst, i8 %value) {
+; CHECK-LABEL: define void @variable_fill_len1_volatile(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]]) {
+; CHECK-NEXT:    store volatile i8 [[VALUE]], ptr [[DST]], align 1
+; CHECK-NEXT:    ret void
+  call void @llvm.memset.p0.i64(ptr align 1 %dst, i8 %value, i64 1, i1 true)
+  ret void
+}
+
+define void @variable_fill_len1_align8(ptr %dst, i8 %value) {
+; CHECK-LABEL: define void @variable_fill_len1_align8(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]]) {
+; CHECK-NEXT:    store i8 [[VALUE]], ptr [[DST]], align 8
+; CHECK-NEXT:    ret void
+  call void @llvm.memset.p0.i64(ptr align 8 %dst, i8 %value, i64 1, i1 false)
+  ret void
+}
+
+define void @variable_fill_len1_addrspace(ptr addrspace(1) %dst, i8 %value) {
+; CHECK-LABEL: define void @variable_fill_len1_addrspace(
+; CHECK-SAME: ptr addrspace(1) [[DST:%.*]], i8 [[VALUE:%.*]]) {
+; CHECK-NEXT:    store i8 [[VALUE]], ptr addrspace(1) [[DST]], align 1
+; CHECK-NEXT:    ret void
+  call void @llvm.memset.p1.i64(ptr addrspace(1) align 1 %dst, i8 %value, i64 1, i1 false)
+  ret void
+}
+
+define void @variable_fill_len1_atomic(ptr %dst, i8 %value) {
+; CHECK-LABEL: define void @variable_fill_len1_atomic(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]]) {
+; CHECK-NEXT:    store atomic i8 [[VALUE]], ptr [[DST]] unordered, align 1
+; CHECK-NEXT:    ret void
+  call void @llvm.memset.element.unordered.atomic.p0.i64(ptr align 1 %dst, i8 %value, i64 1, i32 1)
+  ret void
+}
+
+define void @variable_fill_len2(ptr %dst, i8 %value) {
+; CHECK-LABEL: define void @variable_fill_len2(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]]) {
+; CHECK-NEXT:    call void @llvm.memset.p0.i64(ptr {{.*}}[[DST]], i8 [[VALUE]], i64 2, i1 false)
+; CHECK-NEXT:    ret void
+  call void @llvm.memset.p0.i64(ptr align 1 %dst, i8 %value, i64 2, i1 false)
+  ret void
+}

>From f46d6d130ead9ee55f10116d0474a96b596077fd Mon Sep 17 00:00:00 2001
From: Naveen <naveen.siddegowda at oss.qualcomm.com>
Date: Fri, 31 Jul 2026 03:03:16 -0700
Subject: [PATCH 2/2] [CVP] Guard memset with length in [0, 1]

Use LazyValueInfo to identify memset calls whose length is known to be
in the unsigned range [0, 1].

Insert a conditional branch around the memset and specialize the
executed path to a constant length of one. A following InstCombine pass
can then replace the length-one memset with a byte store including when
the fill value is nonconstant.

Do not transform wider ranges such as [0, 2].

Fixes #213027.

Assisted by GPT-5

Signed-off-by: Naveen <naveen.siddegowda at oss.qualcomm.com>
---
 .../Scalar/CorrelatedValuePropagation.cpp     | 42 ++++++++++
 .../CorrelatedValuePropagation/memset.ll      | 78 +++++++++++++++++++
 2 files changed, 120 insertions(+)
 create mode 100644 llvm/test/Transforms/CorrelatedValuePropagation/memset.ll

diff --git a/llvm/lib/Transforms/Scalar/CorrelatedValuePropagation.cpp b/llvm/lib/Transforms/Scalar/CorrelatedValuePropagation.cpp
index ff0b70b51e5f7..81648a821d8fe 100644
--- a/llvm/lib/Transforms/Scalar/CorrelatedValuePropagation.cpp
+++ b/llvm/lib/Transforms/Scalar/CorrelatedValuePropagation.cpp
@@ -40,6 +40,7 @@
 #include "llvm/IR/Type.h"
 #include "llvm/IR/Value.h"
 #include "llvm/Support/Casting.h"
+#include "llvm/Transforms/Utils/BasicBlockUtils.h"
 #include "llvm/Transforms/Utils/Local.h"
 #include <cassert>
 #include <optional>
@@ -94,6 +95,7 @@ STATISTIC(NumSMinMax,
 STATISTIC(NumUDivURemsNarrowedExpanded,
           "Number of bound udiv's/urem's expanded");
 STATISTIC(NumNNeg, "Number of zext/uitofp non-negative deductions");
+STATISTIC(NumMemSetsGuarded, "Number of memsets guarded for a zero length");
 
 static Constant *getConstantAt(Value *V, Instruction *At, LazyValueInfo *LVI) {
   if (Constant *C = LVI->getConstant(V, At))
@@ -680,6 +682,22 @@ static bool processSaturatingInst(SaturatingInst *SI, LazyValueInfo *LVI) {
   return true;
 }
 
+// Return the nonzero length when MI's length is known to be in [0, 1].
+// Return nullptr for constants and for ranges containing any other value.
+static ConstantInt *getMemSetNonZeroLength(MemSetInst *MI, LazyValueInfo *LVI) {
+  Value *Len = MI->getLength();
+  auto *LenTy = dyn_cast<IntegerType>(Len->getType());
+  if (!LenTy || isa<ConstantInt>(Len))
+    return nullptr;
+
+  ConstantRange Range = LVI->getConstantRangeAtUse(MI->getArgOperandUse(2),
+                                                   /*UndefAllowed=*/false);
+  if (!Range.getUnsignedMin().isZero() || !Range.getUnsignedMax().isOne())
+    return nullptr;
+
+  return ConstantInt::get(LenTy, 1);
+}
+
 /// Infer nonnull attributes for the arguments at the specified callsite.
 static bool processCallSite(CallBase &CB, LazyValueInfo *LVI) {
 
@@ -1264,6 +1282,7 @@ static bool processTrunc(TruncInst *TI, LazyValueInfo *LVI) {
 static bool runImpl(Function &F, LazyValueInfo *LVI, DominatorTree *DT,
                     const SimplifyQuery &SQ) {
   bool FnChanged = false;
+  SmallVector<std::pair<MemSetInst *, ConstantInt *>, 4> MemSetsToGuard;
   std::optional<ConstantRange> RetRange;
   if (F.hasExactDefinition() && F.getReturnType()->isIntOrIntVectorTy())
     RetRange =
@@ -1290,6 +1309,9 @@ static bool runImpl(Function &F, LazyValueInfo *LVI, DominatorTree *DT,
         break;
       case Instruction::Call:
       case Instruction::Invoke:
+        if (auto *MI = dyn_cast<MemSetInst>(&II))
+          if (ConstantInt *NonZeroLen = getMemSetNonZeroLength(MI, LVI))
+            MemSetsToGuard.emplace_back(MI, NonZeroLen);
         BBChanged |= processCallSite(cast<CallBase>(II), LVI);
         break;
       case Instruction::SRem:
@@ -1359,6 +1381,26 @@ static bool runImpl(Function &F, LazyValueInfo *LVI, DominatorTree *DT,
     FnChanged |= BBChanged;
   }
 
+  // Query all ranges before changing the CFG.  LVI is not used after this
+  // point while the dominator tree is updated for each inserted guard.
+  if (!MemSetsToGuard.empty()) {
+    DomTreeUpdater DTU(*DT, DomTreeUpdater::UpdateStrategy::Lazy);
+    for (auto [MI, NonZeroLen] : MemSetsToGuard) {
+      IRBuilder<> B(MI);
+      B.SetCurrentDebugLocation(MI->getDebugLoc());
+      Value *IsNonZero = B.CreateICmpNE(
+          MI->getLength(), ConstantInt::get(MI->getLength()->getType(), 0),
+          "memset.notzero");
+      Instruction *ThenTerm = SplitBlockAndInsertIfThen(
+          IsNonZero, MI->getIterator(), /*Unreachable=*/false,
+          /*BranchWeights=*/nullptr, &DTU);
+      MI->moveBefore(ThenTerm);
+      MI->setLength(NonZeroLen);
+      ++NumMemSetsGuarded;
+    }
+    FnChanged = true;
+  }
+
   // Infer range attribute on return value.
   if (RetRange && !RetRange->isFullSet()) {
     Attribute RangeAttr = F.getRetAttribute(Attribute::Range);
diff --git a/llvm/test/Transforms/CorrelatedValuePropagation/memset.ll b/llvm/test/Transforms/CorrelatedValuePropagation/memset.ll
new file mode 100644
index 0000000000000..fad1bed6fc255
--- /dev/null
+++ b/llvm/test/Transforms/CorrelatedValuePropagation/memset.ll
@@ -0,0 +1,78 @@
+; RUN: opt -passes=correlated-propagation -S < %s | FileCheck %s
+; RUN: opt -passes='correlated-propagation,instcombine' -S < %s | FileCheck %s --check-prefix=COMBINED
+
+declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
+
+; A length in [0, 1] is guarded and specialized to one on the call path.
+define void @range_0_1(ptr %dst, i8 %value, i64 %n) {
+; CHECK-LABEL: define void @range_0_1(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]], i64 [[N:%.*]]) {
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[LEN:%.*]] = and i64 [[N]], 1
+; CHECK-NEXT:    [[MEMSET_NOTZERO:%.*]] = icmp ne i64 [[LEN]], 0
+; CHECK-NEXT:    br i1 [[MEMSET_NOTZERO]], label %[[DO_MEMSET:.*]], label %[[END:.*]]
+; CHECK:       [[DO_MEMSET]]:
+; CHECK-NEXT:    call void @llvm.memset.p0.i64(ptr align 1 [[DST]], i8 [[VALUE]], i64 1, i1 false)
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret void
+; COMBINED-LABEL: define void @range_0_1(
+; COMBINED-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]], i64 [[N:%.*]]) {
+; COMBINED:      [[LEN:%.*]] = and i64 [[N]], 1
+; COMBINED:      [[ISZERO:%.*]] = icmp eq i64 [[LEN]], 0
+; COMBINED:      br i1 [[ISZERO]], label %[[END_BB:.]], label %[[STORE_BB:.]]
+; COMBINED:      [[STORE_BB]]:
+; COMBINED-NEXT: store i8 [[VALUE]], ptr [[DST]], align 1
+; COMBINED-NEXT: br label %[[END_BB]]
+; COMBINED:      [[END_BB]]:
+; COMBINED-NOT:  @llvm.memset
+; COMBINED:      ret void
+entry:
+  %len = and i64 %n, 1
+  call void @llvm.memset.p0.i64(ptr align 1 %dst, i8 %value, i64 %len, i1 false)
+  ret void
+}
+
+; Volatile memsets keep volatile memory semantics after scalarization.
+define void @range_0_1_volatile(ptr %dst, i8 %value, i64 %n) {
+; CHECK-LABEL: define void @range_0_1_volatile(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]], i64 [[N:%.*]]) {
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[LEN:%.*]] = and i64 [[N]], 1
+; CHECK-NEXT:    [[MEMSET_NOTZERO:%.*]] = icmp ne i64 [[LEN]], 0
+; CHECK-NEXT:    br i1 [[MEMSET_NOTZERO]], label %[[DO_MEMSET:.*]], label %[[END:.*]]
+; CHECK:       [[DO_MEMSET]]:
+; CHECK-NEXT:    call void @llvm.memset.p0.i64(ptr align 1 [[DST]], i8 [[VALUE]], i64 1, i1 true)
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret void
+; COMBINED-LABEL: define void @range_0_1_volatile(
+; COMBINED-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]], i64 [[N:%.*]]) {
+; COMBINED:      [[LEN:%.*]] = and i64 [[N]], 1
+; COMBINED:      [[ISZERO:%.*]] = icmp eq i64 [[LEN]], 0
+; COMBINED:      br i1 [[ISZERO]], label %[[END_BB:.]], label %[[STORE_BB:.]]
+; COMBINED:      [[STORE_BB]]:
+; COMBINED-NEXT: store volatile i8 [[VALUE]], ptr [[DST]], align 1
+; COMBINED-NEXT: br label %[[END_BB]]
+; COMBINED:      [[END_BB]]:
+; COMBINED-NOT:  @llvm.memset
+; COMBINED:      ret void
+entry:
+  %len = and i64 %n, 1
+  call void @llvm.memset.p0.i64(ptr align 1 %dst, i8 %value, i64 %len, i1 true)
+  ret void
+}
+
+; The interval [0, 2] contains three values and must not be guarded.
+define void @range_0_2(ptr %dst, i8 %value, i64 %n) {
+; CHECK-LABEL: define void @range_0_2(
+; CHECK-SAME: ptr [[DST:%.*]], i8 [[VALUE:%.*]], i64 [[N:%.*]]) {
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[LEN:%.*]] = urem i64 [[N]], 3
+; CHECK-NEXT:    call void @llvm.memset.p0.i64(ptr align 1 [[DST]], i8 [[VALUE]], i64 [[LEN]], i1 false)
+; CHECK-NEXT:    ret void
+entry:
+  %len = urem i64 %n, 3
+  call void @llvm.memset.p0.i64(ptr align 1 %dst, i8 %value, i64 %len, i1 false)
+  ret void
+}



More information about the llvm-commits mailing list