[llvm] [InstCombine] Recognize strcmp/strncmp/memcmp/bcmp == 0 as commutative (PR #213545)

Aayush Shrivastava via llvm-commits llvm-commits at lists.llvm.org
Fri Aug 14 04:26:54 PDT 2026


https://github.com/iamaayushrivastava updated https://github.com/llvm/llvm-project/pull/213545

>From b9834fd292ead40b87d892d1e7c6f32e954f3f6c Mon Sep 17 00:00:00 2001
From: iamaayushrivastava <iamaayushrivastava at gmail.com>
Date: Sun, 2 Aug 2026 18:50:03 +0530
Subject: [PATCH 1/2] [InstCombine] Recognize strcmp/strncmp/memcmp/bcmp == 0
 as commutative

---
 .../InstCombine/InstCombineCalls.cpp          | 131 ++++++++
 .../InstCombine/InstCombineInternal.h         |   1 +
 .../strcmp-memcmp-commutative-cmp-zero.ll     | 287 ++++++++++++++++++
 3 files changed, 419 insertions(+)
 create mode 100644 llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
index 5ee5009bd0262..614410c5ed85d 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
@@ -4955,6 +4955,134 @@ bool InstCombinerImpl::annotateAnyAllocSite(CallBase &Call,
   return Changed;
 }
 
+/// Returns true if every use of \p CI is an equality (eq/ne) comparison
+/// against the constant zero. strcmp(x, y) == 0, strncmp(x, y, n) == 0,
+/// memcmp(x, y, n) == 0, and bcmp(x, y, n) == 0 all hold under exactly the
+/// same condition as their argument-swapped counterparts (the compared
+/// ranges are equal), even though the concrete nonzero return value of
+/// these functions is implementation-defined and may differ when the
+/// pointer arguments are swapped. So this property is what makes it safe to
+/// treat such a call as interchangeable with its argument-swapped sibling.
+static bool isOnlyUsedInZeroEqualityComparisons(CallInst *CI) {
+  if (CI->use_empty())
+    return false;
+  return all_of(CI->users(), [CI](User *U) {
+    auto *Cmp = dyn_cast<ICmpInst>(U);
+    if (!Cmp || !Cmp->isEquality())
+      return false;
+    Value *Other =
+        Cmp->getOperand(0) == CI ? Cmp->getOperand(1) : Cmp->getOperand(0);
+    return match(Other, PatternMatch::m_Zero());
+  });
+}
+
+/// Returns true if no instruction strictly between \p Dominator and
+/// \p Dominated may write to memory, so the two calls are guaranteed to see
+/// the same memory contents. Only the common shapes that matter in practice
+/// are handled: the two calls share a basic block, or \p Dominated's block
+/// is reached directly (and only) from \p Dominator's block, as happens for
+/// short-circuiting `&&`/`||`.
+static bool isFreeOfIntermediateWrites(CallInst *Dominator,
+                                       CallInst *Dominated) {
+  BasicBlock *DominatorBB = Dominator->getParent();
+  BasicBlock *DominatedBB = Dominated->getParent();
+
+  if (DominatorBB == DominatedBB) {
+    for (Instruction &I : make_range(std::next(Dominator->getIterator()),
+                                     Dominated->getIterator()))
+      if (I.mayWriteToMemory())
+        return false;
+    return true;
+  }
+
+  if (DominatedBB->getUniquePredecessor() != DominatorBB)
+    return false;
+
+  for (Instruction &I :
+       make_range(std::next(Dominator->getIterator()), DominatorBB->end()))
+    if (I.mayWriteToMemory())
+      return false;
+  for (Instruction &I :
+       make_range(DominatedBB->begin(), Dominated->getIterator()))
+    if (I.mayWriteToMemory())
+      return false;
+  return true;
+}
+
+/// strcmp(x, y) == 0, strncmp(x, y, n) == 0, memcmp(x, y, n) == 0, and
+/// bcmp(x, y, n) == 0 are all commutative. They hold precisely when the
+/// compared ranges are equal, regardless of which pointer is passed first.
+/// When we find two such calls in the same function whose pointer
+/// arguments are swapped, and whose results are only ever compared for
+/// equality with zero, fold away the one that is dominated by the other
+/// (memory permitting), exposing the redundancy that ordinary CSE cannot
+/// see because the two calls are not literally identical instructions.
+Instruction *InstCombinerImpl::foldCommutativeCmpLibCall(CallInst &CI) {
+  LibFunc Func;
+  if (!TLI.getLibFunc(CI, Func))
+    return nullptr;
+
+  switch (Func) {
+  case LibFunc_strcmp:
+  case LibFunc_strncmp:
+  case LibFunc_memcmp:
+  case LibFunc_bcmp:
+    break;
+  default:
+    return nullptr;
+  }
+
+  if (!isOnlyUsedInZeroEqualityComparisons(&CI))
+    return nullptr;
+
+  Function *Callee = CI.getCalledFunction();
+  Value *A = CI.getArgOperand(0);
+  Value *B = CI.getArgOperand(1);
+  if (A == B)
+    return nullptr;
+
+  // ConstantData (null, poison, ...) does not maintain a use list, and may
+  // in any case be shared by an unbounded number of unrelated instructions,
+  // so it isn't useful to search its users for a matching call.
+  if (!A->hasUseList())
+    return nullptr;
+
+  for (User *U : A->users()) {
+    auto *Other = dyn_cast<CallInst>(U);
+    if (!Other || Other == &CI || Other->getCalledFunction() != Callee)
+      continue;
+    if (Other->getArgOperand(0) != B || Other->getArgOperand(1) != A)
+      continue;
+    if (CI.arg_size() == 3 && Other->getArgOperand(2) != CI.getArgOperand(2))
+      continue;
+    if (!isOnlyUsedInZeroEqualityComparisons(Other))
+      continue;
+
+    CallInst *Dominator, *Dominated;
+    if (DT.dominates(&CI, Other)) {
+      Dominator = &CI;
+      Dominated = Other;
+    } else if (DT.dominates(Other, &CI)) {
+      Dominator = Other;
+      Dominated = &CI;
+    } else {
+      continue;
+    }
+
+    if (!isFreeOfIntermediateWrites(Dominator, Dominated))
+      continue;
+
+    if (Dominated == &CI)
+      return replaceInstUsesWith(CI, Dominator);
+
+    replaceInstUsesWith(*Dominated, Dominator);
+    eraseInstFromFunction(*Dominated);
+    return nullptr;
+  }
+
+  return nullptr;
+}
+
 /// Improvements for call, callbr and invoke instructions.
 Instruction *InstCombinerImpl::visitCallBase(CallBase &Call) {
   bool Changed = annotateAnyAllocSite(Call, &TLI);
@@ -5090,6 +5218,9 @@ Instruction *InstCombinerImpl::visitCallBase(CallBase &Call) {
   // this.  None of these calls are seen as possibly dead so go ahead and
   // delete the instruction now.
   if (CallInst *CI = dyn_cast<CallInst>(&Call)) {
+    if (Instruction *I = foldCommutativeCmpLibCall(*CI))
+      return I;
+
     Instruction *I = tryOptimizeCall(CI);
     // If we changed something return the result, etc. Otherwise let
     // the fallthrough check.
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineInternal.h b/llvm/lib/Transforms/InstCombine/InstCombineInternal.h
index 8b759e701da60..fcd32b8d4e4cb 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineInternal.h
+++ b/llvm/lib/Transforms/InstCombine/InstCombineInternal.h
@@ -257,6 +257,7 @@ class LLVM_LIBRARY_VISIBILITY InstCombinerImpl final
 
   Instruction *visitCallBase(CallBase &Call);
   Instruction *tryOptimizeCall(CallInst *CI);
+  Instruction *foldCommutativeCmpLibCall(CallInst &CI);
   bool transformConstExprCastCall(CallBase &Call);
   Instruction *transformCallThroughTrampoline(CallBase &Call,
                                               IntrinsicInst &Tramp);
diff --git a/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll b/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll
new file mode 100644
index 0000000000000..758b46394751c
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll
@@ -0,0 +1,287 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+; strcmp(x, y) == 0, strncmp(x, y, n) == 0, memcmp(x, y, n) == 0, and
+; bcmp(x, y, n) == 0 are all commutative: they hold precisely when the
+; compared ranges are equal, regardless of which pointer comes first. When
+; two such calls in a function have their pointer arguments swapped and are
+; only ever compared for equality with zero, the dominated call should be
+; folded away in favor of the dominating one.
+
+declare i32 @strcmp(ptr, ptr)
+declare i32 @strncmp(ptr, ptr, i64)
+declare i32 @memcmp(ptr, ptr, i64)
+declare i32 @bcmp(ptr, ptr, i64)
+
+; (strcmp(x, y) == 0) && (strcmp(y, x) == 0)
+define i1 @strcmp_and(ptr %x, ptr %y, i1 %c) {
+; CHECK-LABEL: define i1 @strcmp_and(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i1 [[C:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i1 [[CMP1]]
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; (strcmp(y, x) == 0) && (strcmp(x, y) == 0)  (swapped order of the two calls)
+define i1 @strcmp_and_commuted(ptr %x, ptr %y) {
+; CHECK-LABEL: define i1 @strcmp_and_commuted(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[Y]], ptr noundef nonnull dereferenceable(1) [[X]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i1 [[CMP1]]
+;
+  %call1 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; (strcmp(x, y) == 0) == (strcmp(y, x) == 0) -> true
+define i1 @strcmp_eq_same_block(ptr %x, ptr %y) {
+; CHECK-LABEL: define i1 @strcmp_eq_same_block(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    ret i1 true
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp eq i32 %call1, 0
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp eq i32 %call2, 0
+  %r = icmp eq i1 %cmp1, %cmp2
+  ret i1 %r
+}
+
+; strncmp(x, y, n) == 0 && strncmp(y, x, n) == 0
+define i1 @strncmp_and(ptr %x, ptr %y, i64 %n) {
+; CHECK-LABEL: define i1 @strncmp_and(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i64 [[N:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strncmp(ptr [[X]], ptr [[Y]], i64 [[N]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i1 [[CMP1]]
+;
+  %call1 = call i32 @strncmp(ptr %x, ptr %y, i64 %n)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strncmp(ptr %y, ptr %x, i64 %n)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; memcmp(x, y, n) == 0 && memcmp(y, x, n) == 0
+define i1 @memcmp_and(ptr %x, ptr %y, i64 %n) {
+; CHECK-LABEL: define i1 @memcmp_and(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i64 [[N:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @memcmp(ptr [[X]], ptr [[Y]], i64 [[N]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i1 [[CMP1]]
+;
+  %call1 = call i32 @memcmp(ptr %x, ptr %y, i64 %n)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @memcmp(ptr %y, ptr %x, i64 %n)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; bcmp(x, y, n) == 0 && bcmp(y, x, n) == 0
+define i1 @bcmp_and(ptr %x, ptr %y, i64 %n) {
+; CHECK-LABEL: define i1 @bcmp_and(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i64 [[N:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @bcmp(ptr [[X]], ptr [[Y]], i64 [[N]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i1 [[CMP1]]
+;
+  %call1 = call i32 @bcmp(ptr %x, ptr %y, i64 %n)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @bcmp(ptr %y, ptr %x, i64 %n)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; Negative test: the raw (nonzero) return value of the second call is used
+; for something other than a zero-equality comparison, so the two calls must
+; not be merged -- their nonzero magnitudes are not guaranteed to be related.
+define i32 @strcmp_raw_value_not_folded(ptr %x, ptr %y) {
+; CHECK-LABEL: define i32 @strcmp_raw_value_not_folded(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    [[CALL2:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[Y]], ptr noundef nonnull dereferenceable(1) [[X]])
+; CHECK-NEXT:    [[SUM:%.*]] = add nsw i32 [[CALL1]], [[CALL2]]
+; CHECK-NEXT:    ret i32 [[SUM]]
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %sum = add nsw i32 %call1, %call2
+  ret i32 %sum
+}
+
+; Negative test: the two memcmp calls use different length arguments, so
+; they are not guaranteed to compare the same ranges and must not be merged.
+define i1 @memcmp_different_size_not_folded(ptr %x, ptr %y) {
+; CHECK-LABEL: define i1 @memcmp_different_size_not_folded(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]]) {
+; CHECK-NEXT:    [[LHSC:%.*]] = load i8, ptr [[X]], align 1
+; CHECK-NEXT:    [[RHSC:%.*]] = load i8, ptr [[Y]], align 1
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i8 [[LHSC]], [[RHSC]]
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    [[CALL2:%.*]] = call i32 @memcmp(ptr noundef nonnull dereferenceable(2) [[Y]], ptr noundef nonnull dereferenceable(2) [[X]], i64 2)
+; CHECK-NEXT:    [[CMP2:%.*]] = icmp eq i32 [[CALL2]], 0
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    [[R:%.*]] = phi i1 [ false, [[TMP0:%.*]] ], [ [[CMP2]], %[[RHS]] ]
+; CHECK-NEXT:    ret i1 [[R]]
+;
+  %call1 = call i32 @memcmp(ptr %x, ptr %y, i64 1)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @memcmp(ptr %y, ptr %x, i64 2)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; Negative test: a store that may alias either buffer occurs between the two
+; calls, so the second call cannot be assumed to observe the same memory
+; contents as the first, and the calls must not be merged.
+define i1 @strcmp_clobbered_between_calls_not_folded(ptr %x, ptr %y) {
+; CHECK-LABEL: define i1 @strcmp_clobbered_between_calls_not_folded(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    store i8 97, ptr [[X]], align 1
+; CHECK-NEXT:    [[CALL2:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[Y]], ptr noundef nonnull dereferenceable(1) [[X]])
+; CHECK-NEXT:    [[CMP2:%.*]] = icmp eq i32 [[CALL2]], 0
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    [[R:%.*]] = phi i1 [ false, [[TMP0:%.*]] ], [ [[CMP2]], %[[RHS]] ]
+; CHECK-NEXT:    ret i1 [[R]]
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  store i8 97, ptr %x
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; Negative test: the two calls are in unrelated, non-dominating branches, so
+; neither call is guaranteed to have executed before the other.
+define i1 @strcmp_non_dominating_branches_not_folded(ptr %x, ptr %y, i1 %c) {
+; CHECK-LABEL: define i1 @strcmp_non_dominating_branches_not_folded(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i1 [[C:%.*]]) {
+; CHECK-NEXT:    br i1 [[C]], label %[[BB1:.*]], label %[[BB2:.*]]
+; CHECK:       [[BB1]]:
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    br label %[[END:.*]]
+; CHECK:       [[BB2]]:
+; CHECK-NEXT:    [[CALL3:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[Y]], ptr noundef nonnull dereferenceable(1) [[X]])
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    [[CALL2:%.*]] = phi i32 [ [[CALL1]], %[[BB1]] ], [ [[CALL3]], %[[BB2]] ]
+; CHECK-NEXT:    [[CMP2:%.*]] = icmp eq i32 [[CALL2]], 0
+; CHECK-NEXT:    ret i1 [[CMP2]]
+;
+  br i1 %c, label %bb1, label %bb2
+
+bb1:
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp eq i32 %call1, 0
+  br label %end
+
+bb2:
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp eq i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ %cmp1, %bb1 ], [ %cmp2, %bb2 ]
+  ret i1 %r
+}
+
+; Negative test: strcmp(x, x) has both "arguments" be the same value, this
+; is already folded by the library call simplifier and should not interact
+; with the new fold.
+define i1 @strcmp_same_pointer_not_ambiguous(ptr %x) {
+; CHECK-LABEL: define i1 @strcmp_same_pointer_not_ambiguous(
+; CHECK-SAME: ptr [[X:%.*]]) {
+; CHECK-NEXT:    ret i1 true
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %x)
+  %cmp1 = icmp eq i32 %call1, 0
+  ret i1 %cmp1
+}

>From a4c31d9f5f0988450c4030abddd9fc73efb468ce Mon Sep 17 00:00:00 2001
From: iamaayushrivastava <iamaayushrivastava at gmail.com>
Date: Fri, 14 Aug 2026 16:56:25 +0530
Subject: [PATCH 2/2] [InstCombine] Extend strcmp/memcmp commutative fold to
 signed comparisons

---
 .../InstCombine/InstCombineCalls.cpp          |  78 ++++++-----
 .../strcmp-memcmp-commutative-cmp-zero.ll     | 132 +++++++++++++++++-
 2 files changed, 170 insertions(+), 40 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
index 614410c5ed85d..3ca47d0d0ab18 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineCalls.cpp
@@ -4955,20 +4955,18 @@ bool InstCombinerImpl::annotateAnyAllocSite(CallBase &Call,
   return Changed;
 }
 
-/// Returns true if every use of \p CI is an equality (eq/ne) comparison
-/// against the constant zero. strcmp(x, y) == 0, strncmp(x, y, n) == 0,
-/// memcmp(x, y, n) == 0, and bcmp(x, y, n) == 0 all hold under exactly the
-/// same condition as their argument-swapped counterparts (the compared
-/// ranges are equal), even though the concrete nonzero return value of
-/// these functions is implementation-defined and may differ when the
-/// pointer arguments are swapped. So this property is what makes it safe to
-/// treat such a call as interchangeable with its argument-swapped sibling.
-static bool isOnlyUsedInZeroEqualityComparisons(CallInst *CI) {
+/// True if every use of \p CI is an equality or signed-ordering comparison
+/// against zero. Only the sign of a nonzero strcmp/strncmp/memcmp/bcmp
+/// result is specified, so these are the only predicates whose truth value
+/// survives a sign flip (via CmpInst::getSwappedPredicate()). Unsigned
+/// predicates are excluded: e.g. "x <u 0" is always false regardless of
+/// sign.
+static bool isOnlyUsedInSignAgnosticZeroComparisons(CallInst *CI) {
   if (CI->use_empty())
     return false;
   return all_of(CI->users(), [CI](User *U) {
     auto *Cmp = dyn_cast<ICmpInst>(U);
-    if (!Cmp || !Cmp->isEquality())
+    if (!Cmp || !(Cmp->isEquality() || Cmp->isSigned()))
       return false;
     Value *Other =
         Cmp->getOperand(0) == CI ? Cmp->getOperand(1) : Cmp->getOperand(0);
@@ -4976,12 +4974,10 @@ static bool isOnlyUsedInZeroEqualityComparisons(CallInst *CI) {
   });
 }
 
-/// Returns true if no instruction strictly between \p Dominator and
-/// \p Dominated may write to memory, so the two calls are guaranteed to see
-/// the same memory contents. Only the common shapes that matter in practice
-/// are handled: the two calls share a basic block, or \p Dominated's block
-/// is reached directly (and only) from \p Dominator's block, as happens for
-/// short-circuiting `&&`/`||`.
+/// True if no instruction between \p Dominator and \p Dominated may write
+/// to memory. Only handles the two shapes that matter in practice: same
+/// basic block, or \p Dominated's block reached directly and only from
+/// \p Dominator's block (short-circuiting `&&`/`||`).
 static bool isFreeOfIntermediateWrites(CallInst *Dominator,
                                        CallInst *Dominated) {
   BasicBlock *DominatorBB = Dominator->getParent();
@@ -5009,14 +5005,13 @@ static bool isFreeOfIntermediateWrites(CallInst *Dominator,
   return true;
 }
 
-/// strcmp(x, y) == 0, strncmp(x, y, n) == 0, memcmp(x, y, n) == 0, and
-/// bcmp(x, y, n) == 0 are all commutative. They hold precisely when the
-/// compared ranges are equal, regardless of which pointer is passed first.
-/// When we find two such calls in the same function whose pointer
-/// arguments are swapped, and whose results are only ever compared for
-/// equality with zero, fold away the one that is dominated by the other
-/// (memory permitting), exposing the redundancy that ordinary CSE cannot
-/// see because the two calls are not literally identical instructions.
+/// Swapping the pointer arguments of strcmp/strncmp/memcmp/bcmp negates the
+/// sign of a nonzero result and leaves a zero result unchanged. Given two
+/// such calls in the same function with swapped arguments, fold away
+/// whichever is dominated by the other (memory permitting), reusing the
+/// dominating call's result with its comparisons' predicates swapped to
+/// match. Ordinary CSE can't see this redundancy since the calls aren't
+/// literally identical instructions.
 Instruction *InstCombinerImpl::foldCommutativeCmpLibCall(CallInst &CI) {
   LibFunc Func;
   if (!TLI.getLibFunc(CI, Func))
@@ -5032,18 +5027,13 @@ Instruction *InstCombinerImpl::foldCommutativeCmpLibCall(CallInst &CI) {
     return nullptr;
   }
 
-  if (!isOnlyUsedInZeroEqualityComparisons(&CI))
-    return nullptr;
-
   Function *Callee = CI.getCalledFunction();
   Value *A = CI.getArgOperand(0);
   Value *B = CI.getArgOperand(1);
   if (A == B)
     return nullptr;
 
-  // ConstantData (null, poison, ...) does not maintain a use list, and may
-  // in any case be shared by an unbounded number of unrelated instructions,
-  // so it isn't useful to search its users for a matching call.
+  // ConstantData (null, poison, ...) has no use list to search.
   if (!A->hasUseList())
     return nullptr;
 
@@ -5051,13 +5041,17 @@ Instruction *InstCombinerImpl::foldCommutativeCmpLibCall(CallInst &CI) {
     auto *Other = dyn_cast<CallInst>(U);
     if (!Other || Other == &CI || Other->getCalledFunction() != Callee)
       continue;
+    // A may be a global shared across functions, so Other may not be in CI's
+    // function; DominatorTree queries below require it to be.
+    if (Other->getFunction() != CI.getFunction())
+      continue;
     if (Other->getArgOperand(0) != B || Other->getArgOperand(1) != A)
       continue;
     if (CI.arg_size() == 3 && Other->getArgOperand(2) != CI.getArgOperand(2))
       continue;
-    if (!isOnlyUsedInZeroEqualityComparisons(Other))
-      continue;
 
+    // Only Dominated needs safely-reinterpretable uses; Dominator is left
+    // untouched, so its other uses don't matter.
     CallInst *Dominator, *Dominated;
     if (DT.dominates(&CI, Other)) {
       Dominator = &CI;
@@ -5069,13 +5063,29 @@ Instruction *InstCombinerImpl::foldCommutativeCmpLibCall(CallInst &CI) {
       continue;
     }
 
+    if (!isOnlyUsedInSignAgnosticZeroComparisons(Dominated))
+      continue;
+
     if (!isFreeOfIntermediateWrites(Dominator, Dominated))
       continue;
 
+    // Point each comparison at Dominator instead, swapping its predicate to
+    // compensate for the sign flip (a no-op for eq/ne).
+    for (Use &U2 : make_early_inc_range(Dominated->uses())) {
+      auto *Cmp = cast<ICmpInst>(U2.getUser());
+      Cmp->setPredicate(Cmp->getSwappedPredicate());
+      replaceUse(U2, Dominator);
+      Worklist.pushUsersToWorkList(*Cmp);
+      Worklist.push(Cmp);
+    }
+
+    // Dominated is now unused. Don't erase it here if it's CI:
+    // eraseInstFromFunction() returns nullptr, which the caller would read
+    // as "no change" and keep using the now-freed CI. Returning &CI lets
+    // the main loop's "modified in place" path erase it once dead instead.
     if (Dominated == &CI)
-      return replaceInstUsesWith(CI, Dominator);
+      return &CI;
 
-    replaceInstUsesWith(*Dominated, Dominator);
     eraseInstFromFunction(*Dominated);
     return nullptr;
   }
diff --git a/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll b/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll
index 758b46394751c..11ce50c20d951 100644
--- a/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll
+++ b/llvm/test/Transforms/InstCombine/strcmp-memcmp-commutative-cmp-zero.ll
@@ -1,12 +1,14 @@
 ; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
 ; RUN: opt < %s -passes=instcombine -S | FileCheck %s
 
-; strcmp(x, y) == 0, strncmp(x, y, n) == 0, memcmp(x, y, n) == 0, and
-; bcmp(x, y, n) == 0 are all commutative: they hold precisely when the
-; compared ranges are equal, regardless of which pointer comes first. When
-; two such calls in a function have their pointer arguments swapped and are
-; only ever compared for equality with zero, the dominated call should be
-; folded away in favor of the dominating one.
+; strcmp(x, y), strncmp(x, y, n), memcmp(x, y, n), and bcmp(x, y, n) are all
+; commutative in the sense that swapping their pointer arguments negates the
+; sign of a nonzero result without changing its magnitude, and leaves a zero
+; result unchanged. When two such calls in a function have their pointer
+; arguments swapped and are only ever compared against zero with an
+; equality or signed-ordering predicate, the dominated call should be
+; folded away in favor of the dominating one, swapping the predicate of any
+; ordering comparison to compensate for the sign flip.
 
 declare i32 @strcmp(ptr, ptr)
 declare i32 @strncmp(ptr, ptr, i64)
@@ -158,6 +160,124 @@ end:
   ret i1 %r
 }
 
+; (strcmp(x, y) < 0) and (strcmp(y, x) > 0) are equivalent: swapping the
+; pointer arguments negates the sign of a nonzero result, so the dominated
+; call's `sgt` comparison should be rewritten to an `slt` comparison against
+; the dominating call, per CmpInst::getSwappedPredicate().
+define i1 @strcmp_slt_sgt(ptr %x, ptr %y, i1 %c) {
+; CHECK-LABEL: define i1 @strcmp_slt_sgt(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i1 [[C:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    br i1 [[C]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    [[R:%.*]] = icmp slt i32 [[CALL1]], 0
+; CHECK-NEXT:    ret i1 [[R]]
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp slt i32 %call1, 0
+  br i1 %c, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp sgt i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ %cmp1, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; (strcmp(x, y) <= 0) and (strcmp(y, x) >= 0) are equivalent, exercising the
+; sle/sge half of the swapped-predicate table.
+define i1 @strcmp_sle_sge(ptr %x, ptr %y, i1 %c) {
+; CHECK-LABEL: define i1 @strcmp_sle_sge(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i1 [[C:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    br i1 [[C]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    [[R:%.*]] = icmp slt i32 [[CALL1]], 1
+; CHECK-NEXT:    ret i1 [[R]]
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp sle i32 %call1, 0
+  br i1 %c, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp sge i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ %cmp1, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; A single call's result may be used by a mix of an equality comparison and
+; a signed-ordering comparison; both are sign-agnostic (in the sense used by
+; isOnlyUsedInSignAgnosticZeroComparisons) and should be rewritten together.
+define i1 @strcmp_mixed_eq_and_ordering(ptr %x, ptr %y, i1 %c) {
+; CHECK-LABEL: define i1 @strcmp_mixed_eq_and_ordering(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]], i1 [[C:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[C]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    [[CMP2:%.*]] = icmp sgt i32 [[CALL1]], 0
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    [[R:%.*]] = phi i1 [ [[CMP1]], [[TMP0:%.*]] ], [ [[CMP2]], %[[RHS]] ]
+; CHECK-NEXT:    ret i1 [[R]]
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %c, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp slt i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ %cmp1, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
+; Negative test: an unsigned ordering predicate against zero has no
+; sign-based interpretation ("x <u 0" is simply always false, regardless of
+; the sign of x), so it must not be folded via the swapped-predicate rule.
+; This also regression-tests a use-after-free: when the currently-visited
+; call is the one folded away, the caller must not treat that as "no change"
+; and go on to further optimize the now-erased call.
+define i1 @strcmp_unsigned_ordering_not_folded(ptr %x, ptr %y) {
+; CHECK-LABEL: define i1 @strcmp_unsigned_ordering_not_folded(
+; CHECK-SAME: ptr [[X:%.*]], ptr [[Y:%.*]]) {
+; CHECK-NEXT:    [[CALL1:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[X]], ptr noundef nonnull dereferenceable(1) [[Y]])
+; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i32 [[CALL1]], 0
+; CHECK-NEXT:    br i1 [[CMP1]], label %[[RHS:.*]], label %[[END:.*]]
+; CHECK:       [[RHS]]:
+; CHECK-NEXT:    [[CALL2:%.*]] = call i32 @strcmp(ptr noundef nonnull dereferenceable(1) [[Y]], ptr noundef nonnull dereferenceable(1) [[X]])
+; CHECK-NEXT:    br label %[[END]]
+; CHECK:       [[END]]:
+; CHECK-NEXT:    ret i1 false
+;
+  %call1 = call i32 @strcmp(ptr %x, ptr %y)
+  %cmp1 = icmp eq i32 %call1, 0
+  br i1 %cmp1, label %rhs, label %end
+
+rhs:
+  %call2 = call i32 @strcmp(ptr %y, ptr %x)
+  %cmp2 = icmp ugt i32 %call2, 0
+  br label %end
+
+end:
+  %r = phi i1 [ false, %0 ], [ %cmp2, %rhs ]
+  ret i1 %r
+}
+
 ; Negative test: the raw (nonzero) return value of the second call is used
 ; for something other than a zero-equality comparison, so the two calls must
 ; not be merged -- their nonzero magnitudes are not guaranteed to be related.



More information about the llvm-commits mailing list