[llvm] [GVN] Eliminate redundant scalar binop by reusing vector binop result (PR #186102)
via llvm-commits
llvm-commits at lists.llvm.org
Thu Mar 12 04:54:22 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-transforms
Author: Vedant Neve (0bVdnt)
<details>
<summary>Changes</summary>
Fixes #<!-- -->185564
When a scalar binary operation computes the same thing as one lane of an existing dominating vector binary operation, GVN now replaces it with an `extractelement` from the vector result.
---
Full diff: https://github.com/llvm/llvm-project/pull/186102.diff
2 Files Affected:
- (modified) llvm/lib/Transforms/Scalar/GVN.cpp (+82)
- (added) llvm/test/Transforms/GVN/scalar-binop-extract-redundancy.ll (+278)
``````````diff
diff --git a/llvm/lib/Transforms/Scalar/GVN.cpp b/llvm/lib/Transforms/Scalar/GVN.cpp
index c18cafd75a0a5..c2120a124c94b 100644
--- a/llvm/lib/Transforms/Scalar/GVN.cpp
+++ b/llvm/lib/Transforms/Scalar/GVN.cpp
@@ -2657,6 +2657,39 @@ bool GVNPass::propagateEquality(
return Changed;
}
+/// Return true if every poison-generating flag on VecBO is also present on
+/// ScalarBO. This ensures that replacing the scalar op with an extract from
+/// the vector op is safe: the extracted lane cannot be poison in any case
+/// where the scalar op would not also be poison.
+static bool hasSameOrStricterFlags(BinaryOperator *VecBO,
+ BinaryOperator *ScalarBO) {
+ assert(VecBO->getOpcode() == ScalarBO->getOpcode());
+ if (isa<OverflowingBinaryOperator>(VecBO)) {
+ if (VecBO->hasNoUnsignedWrap() && !ScalarBO->hasNoUnsignedWrap())
+ return false;
+ if (VecBO->hasNoSignedWrap() && !ScalarBO->hasNoSignedWrap())
+ return false;
+ }
+ if (isa<PossiblyExactOperator>(VecBO)) {
+ if (VecBO->isExact() && !ScalarBO->isExact())
+ return false;
+ }
+ if (auto *VPD = dyn_cast<PossiblyDisjointInst>(VecBO)) {
+ if (VPD->isDisjoint() &&
+ !cast<PossiblyDisjointInst>(ScalarBO)->isDisjoint())
+ return false;
+ }
+ if (isa<FPMathOperator>(VecBO)) {
+ FastMathFlags VF = VecBO->getFastMathFlags();
+ FastMathFlags SF = ScalarBO->getFastMathFlags();
+ if (VF.noNaNs() && !SF.noNaNs())
+ return false;
+ if (VF.noInfs() && !SF.noInfs())
+ return false;
+ }
+ return true;
+}
+
/// When calculating availability, handle an instruction
/// by inserting it into the appropriate sets.
bool GVNPass::processInstruction(Instruction *I) {
@@ -2759,6 +2792,55 @@ bool GVNPass::processInstruction(Instruction *I) {
if (I->getType()->isVoidTy())
return false;
+ // Try to replace a scalar binop whose operands are both extracted from
+ // vectors at the same index, with an extractelement from an existing
+ // vector binop of the same kind. For example:
+ // %v6 = fmul <2 x float> %v0, %v3
+ // %a = extractelement %v0, 1
+ // %b = extractelement %v3, 1
+ // %r = fmul float %a, %b ; replaced by extractelement %v6, 1
+ if (auto *BO = dyn_cast<BinaryOperator>(I)) {
+ Value *V0, *V1;
+ uint64_t Idx0, Idx1;
+ if (match(BO->getOperand(0),
+ m_ExtractElt(m_Value(V0), m_ConstantInt(Idx0))) &&
+ match(BO->getOperand(1),
+ m_ExtractElt(m_Value(V1), m_ConstantInt(Idx1))) &&
+ Idx0 == Idx1 && V0->getType() == V1->getType()) {
+ for (User *U : V0->users()) {
+ auto *VecBO = dyn_cast<BinaryOperator>(U);
+ if (!VecBO || VecBO->getOpcode() != BO->getOpcode())
+ continue;
+ bool Match =
+ (VecBO->getOperand(0) == V0 && VecBO->getOperand(1) == V1) ||
+ (VecBO->isCommutative() && VecBO->getOperand(0) == V1 &&
+ VecBO->getOperand(1) == V0);
+ if (!Match)
+ continue;
+ if (!DT->dominates(VecBO, BO))
+ continue;
+ // The vector binop must not have poison-generating flags that the
+ // scalar binop does not, otherwise the extracted lane could be poison
+ // where the scalar result would not be.
+ if (VecBO->hasPoisonGeneratingFlags() &&
+ !hasSameOrStricterFlags(VecBO, BO))
+ continue;
+ // Create extractelement to reuse the vector result.
+ Value *IdxVal =
+ cast<ExtractElementInst>(BO->getOperand(0))->getIndexOperand();
+ auto *Ext = ExtractElementInst::Create(VecBO, IdxVal);
+ Ext->insertBefore(BO->getIterator());
+ Ext->setDebugLoc(BO->getDebugLoc());
+ // Value-number the new instruction and register it.
+ unsigned ExtNum = VN.lookupOrAdd(Ext);
+ LeaderTable.insert(ExtNum, Ext, Ext->getParent());
+ patchAndReplaceAllUsesWith(BO, Ext);
+ salvageAndRemoveInstruction(BO);
+ return true;
+ }
+ }
+ }
+
uint32_t NextNum = VN.getNextUnusedValueNumber();
unsigned Num = VN.lookupOrAdd(I);
diff --git a/llvm/test/Transforms/GVN/scalar-binop-extract-redundancy.ll b/llvm/test/Transforms/GVN/scalar-binop-extract-redundancy.ll
new file mode 100644
index 0000000000000..075b76d2ceb67
--- /dev/null
+++ b/llvm/test/Transforms/GVN/scalar-binop-extract-redundancy.ll
@@ -0,0 +1,278 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt -passes=gvn -S < %s | FileCheck %s
+
+; When a scalar binop computes the same thing as an element of an existing
+; vector binop, GVN should replace it with an extractelement.
+
+; The motivating example from issue #185564: the scalar fmul of extracted
+; elements should be replaced by an extractelement from the vector fmul.
+define i1 @src(ptr %arg0, ptr %arg1) {
+; CHECK-LABEL: @src(
+; CHECK-NEXT: [[V0:%.*]] = load <2 x float>, ptr [[ARG1:%.*]], align 4
+; CHECK-NEXT: [[V1:%.*]] = getelementptr inbounds nuw i8, ptr [[ARG1]], i64 8
+; CHECK-NEXT: [[V2:%.*]] = load float, ptr [[V1]], align 4
+; CHECK-NEXT: [[V3:%.*]] = load <2 x float>, ptr [[ARG0:%.*]], align 4
+; CHECK-NEXT: [[V4:%.*]] = getelementptr inbounds nuw i8, ptr [[ARG0]], i64 8
+; CHECK-NEXT: [[V5:%.*]] = load float, ptr [[V4]], align 4
+; CHECK-NEXT: [[V6:%.*]] = fmul <2 x float> [[V0]], [[V3]]
+; CHECK-NEXT: [[V7:%.*]] = extractelement <2 x float> [[V6]], i64 0
+; CHECK-NEXT: [[V8:%.*]] = extractelement <2 x float> [[V0]], i64 1
+; CHECK-NEXT: [[V9:%.*]] = extractelement <2 x float> [[V3]], i64 1
+; CHECK-NEXT: [[TMP1:%.*]] = extractelement <2 x float> [[V6]], i64 1
+; CHECK-NEXT: [[V11:%.*]] = fadd float [[V7]], [[TMP1]]
+; CHECK-NEXT: [[V12:%.*]] = fmul float [[V2]], [[V5]]
+; CHECK-NEXT: [[V13:%.*]] = fadd float [[V12]], [[V11]]
+; CHECK-NEXT: [[V14:%.*]] = fcmp olt float [[V13]], 0.000000e+00
+; CHECK-NEXT: ret i1 [[V14]]
+;
+ %v0 = load <2 x float>, ptr %arg1, align 4
+ %v1 = getelementptr inbounds nuw i8, ptr %arg1, i64 8
+ %v2 = load float, ptr %v1, align 4
+ %v3 = load <2 x float>, ptr %arg0, align 4
+ %v4 = getelementptr inbounds nuw i8, ptr %arg0, i64 8
+ %v5 = load float, ptr %v4, align 4
+ %v6 = fmul <2 x float> %v0, %v3
+ %v7 = extractelement <2 x float> %v6, i64 0
+ %v8 = extractelement <2 x float> %v0, i64 1
+ %v9 = extractelement <2 x float> %v3, i64 1
+ %v10 = fmul float %v8, %v9
+ %v11 = fadd float %v7, %v10
+ %v12 = fmul float %v2, %v5
+ %v13 = fadd float %v12, %v11
+ %v14 = fcmp olt float %v13, 0.000000e+00
+ ret i1 %v14
+}
+
+; The already-optimised form from issue #185564 — GVN should leave it alone.
+define i1 @tgt(ptr %arg0, ptr %arg1) {
+; CHECK-LABEL: @tgt(
+; CHECK-NEXT: [[V0:%.*]] = load <2 x float>, ptr [[ARG1:%.*]], align 4
+; CHECK-NEXT: [[V1:%.*]] = getelementptr inbounds nuw i8, ptr [[ARG1]], i64 8
+; CHECK-NEXT: [[V2:%.*]] = load float, ptr [[V1]], align 4
+; CHECK-NEXT: [[V3:%.*]] = load <2 x float>, ptr [[ARG0:%.*]], align 4
+; CHECK-NEXT: [[V4:%.*]] = getelementptr inbounds nuw i8, ptr [[ARG0]], i64 8
+; CHECK-NEXT: [[V5:%.*]] = load float, ptr [[V4]], align 4
+; CHECK-NEXT: [[V6:%.*]] = fmul <2 x float> [[V0]], [[V3]]
+; CHECK-NEXT: [[V7:%.*]] = extractelement <2 x float> [[V6]], i64 0
+; CHECK-NEXT: [[V8:%.*]] = extractelement <2 x float> [[V6]], i64 1
+; CHECK-NEXT: [[V9:%.*]] = fadd float [[V7]], [[V8]]
+; CHECK-NEXT: [[V10:%.*]] = fmul float [[V2]], [[V5]]
+; CHECK-NEXT: [[V11:%.*]] = fadd float [[V10]], [[V9]]
+; CHECK-NEXT: [[V12:%.*]] = fcmp olt float [[V11]], 0.000000e+00
+; CHECK-NEXT: ret i1 [[V12]]
+;
+ %v0 = load <2 x float>, ptr %arg1, align 4
+ %v1 = getelementptr inbounds nuw i8, ptr %arg1, i64 8
+ %v2 = load float, ptr %v1, align 4
+ %v3 = load <2 x float>, ptr %arg0, align 4
+ %v4 = getelementptr inbounds nuw i8, ptr %arg0, i64 8
+ %v5 = load float, ptr %v4, align 4
+ %v6 = fmul <2 x float> %v0, %v3
+ %v7 = extractelement <2 x float> %v6, i64 0
+ %v8 = extractelement <2 x float> %v6, i64 1
+ %v9 = fadd float %v7, %v8
+ %v10 = fmul float %v2, %v5
+ %v11 = fadd float %v10, %v9
+ %v12 = fcmp olt float %v11, 0.000000e+00
+ ret i1 %v12
+}
+
+; Integer binop (add) with the same pattern.
+define i32 @add_extract_redundant(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @add_extract_redundant(
+; CHECK-NEXT: [[VEC_ADD:%.*]] = add <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[A2:%.*]] = extractelement <4 x i32> [[A]], i64 2
+; CHECK-NEXT: [[B2:%.*]] = extractelement <4 x i32> [[B]], i64 2
+; CHECK-NEXT: [[TMP1:%.*]] = extractelement <4 x i32> [[VEC_ADD]], i64 2
+; CHECK-NEXT: ret i32 [[TMP1]]
+;
+ %vec_add = add <4 x i32> %a, %b
+ %a2 = extractelement <4 x i32> %a, i64 2
+ %b2 = extractelement <4 x i32> %b, i64 2
+ %scalar_add = add i32 %a2, %b2
+ ret i32 %scalar_add
+}
+
+; Commutative: vector is mul(%a, %b), scalar is mul(%b_elem, %a_elem).
+define i32 @mul_commuted(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @mul_commuted(
+; CHECK-NEXT: [[VEC_MUL:%.*]] = mul <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[B1:%.*]] = extractelement <4 x i32> [[B]], i64 1
+; CHECK-NEXT: [[A1:%.*]] = extractelement <4 x i32> [[A]], i64 1
+; CHECK-NEXT: [[TMP1:%.*]] = extractelement <4 x i32> [[VEC_MUL]], i64 1
+; CHECK-NEXT: ret i32 [[TMP1]]
+;
+ %vec_mul = mul <4 x i32> %a, %b
+ %b1 = extractelement <4 x i32> %b, i64 1
+ %a1 = extractelement <4 x i32> %a, i64 1
+ %scalar_mul = mul i32 %b1, %a1
+ ret i32 %scalar_mul
+}
+
+; Negative test: different indices — should NOT be optimized.
+define i32 @different_indices(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @different_indices(
+; CHECK-NEXT: [[VEC_ADD:%.*]] = add <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[A1:%.*]] = extractelement <4 x i32> [[A]], i64 1
+; CHECK-NEXT: [[B2:%.*]] = extractelement <4 x i32> [[B]], i64 2
+; CHECK-NEXT: [[SCALAR_ADD:%.*]] = add i32 [[A1]], [[B2]]
+; CHECK-NEXT: ret i32 [[SCALAR_ADD]]
+;
+ %vec_add = add <4 x i32> %a, %b
+ %a1 = extractelement <4 x i32> %a, i64 1
+ %b2 = extractelement <4 x i32> %b, i64 2
+ %scalar_add = add i32 %a1, %b2
+ ret i32 %scalar_add
+}
+
+; Negative test: no vector binop exists — should NOT be optimized.
+define i32 @no_vector_binop(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @no_vector_binop(
+; CHECK-NEXT: [[A2:%.*]] = extractelement <4 x i32> [[A:%.*]], i64 2
+; CHECK-NEXT: [[B2:%.*]] = extractelement <4 x i32> [[B:%.*]], i64 2
+; CHECK-NEXT: [[SCALAR_ADD:%.*]] = add i32 [[A2]], [[B2]]
+; CHECK-NEXT: ret i32 [[SCALAR_ADD]]
+;
+ %a2 = extractelement <4 x i32> %a, i64 2
+ %b2 = extractelement <4 x i32> %b, i64 2
+ %scalar_add = add i32 %a2, %b2
+ ret i32 %scalar_add
+}
+
+; Negative test: non-commutative op (sub) with swapped operands.
+define i32 @sub_not_commutative(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @sub_not_commutative(
+; CHECK-NEXT: [[VEC_SUB:%.*]] = sub <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[B1:%.*]] = extractelement <4 x i32> [[B]], i64 1
+; CHECK-NEXT: [[A1:%.*]] = extractelement <4 x i32> [[A]], i64 1
+; CHECK-NEXT: [[SCALAR_SUB:%.*]] = sub i32 [[B1]], [[A1]]
+; CHECK-NEXT: ret i32 [[SCALAR_SUB]]
+;
+ %vec_sub = sub <4 x i32> %a, %b
+ %b1 = extractelement <4 x i32> %b, i64 1
+ %a1 = extractelement <4 x i32> %a, i64 1
+ %scalar_sub = sub i32 %b1, %a1
+ ret i32 %scalar_sub
+}
+
+; Vector binop has nuw but scalar does not — should NOT use vector result.
+define i32 @poison_flags_mismatch(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @poison_flags_mismatch(
+; CHECK-NEXT: [[VEC_ADD:%.*]] = add nuw <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[A2:%.*]] = extractelement <4 x i32> [[A]], i64 2
+; CHECK-NEXT: [[B2:%.*]] = extractelement <4 x i32> [[B]], i64 2
+; CHECK-NEXT: [[SCALAR_ADD:%.*]] = add i32 [[A2]], [[B2]]
+; CHECK-NEXT: ret i32 [[SCALAR_ADD]]
+;
+ %vec_add = add nuw <4 x i32> %a, %b
+ %a2 = extractelement <4 x i32> %a, i64 2
+ %b2 = extractelement <4 x i32> %b, i64 2
+ %scalar_add = add i32 %a2, %b2
+ ret i32 %scalar_add
+}
+
+; Both have nuw — should optimize.
+define i32 @matching_nuw_flags(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @matching_nuw_flags(
+; CHECK-NEXT: [[VEC_ADD:%.*]] = add nuw <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[A2:%.*]] = extractelement <4 x i32> [[A]], i64 2
+; CHECK-NEXT: [[B2:%.*]] = extractelement <4 x i32> [[B]], i64 2
+; CHECK-NEXT: [[TMP1:%.*]] = extractelement <4 x i32> [[VEC_ADD]], i64 2
+; CHECK-NEXT: ret i32 [[TMP1]]
+;
+ %vec_add = add nuw <4 x i32> %a, %b
+ %a2 = extractelement <4 x i32> %a, i64 2
+ %b2 = extractelement <4 x i32> %b, i64 2
+ %scalar_add = add nuw i32 %a2, %b2
+ ret i32 %scalar_add
+}
+
+; Negative test: vector fmul has nnan but scalar does not.
+define float @nnan_flag_mismatch(<2 x float> %a, <2 x float> %b) {
+; CHECK-LABEL: @nnan_flag_mismatch(
+; CHECK-NEXT: [[VEC_MUL:%.*]] = fmul nnan <2 x float> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[A0:%.*]] = extractelement <2 x float> [[A]], i64 0
+; CHECK-NEXT: [[B0:%.*]] = extractelement <2 x float> [[B]], i64 0
+; CHECK-NEXT: [[SCALAR_MUL:%.*]] = fmul float [[A0]], [[B0]]
+; CHECK-NEXT: ret float [[SCALAR_MUL]]
+;
+ %vec_mul = fmul nnan <2 x float> %a, %b
+ %a0 = extractelement <2 x float> %a, i64 0
+ %b0 = extractelement <2 x float> %b, i64 0
+ %scalar_mul = fmul float %a0, %b0
+ ret float %scalar_mul
+}
+
+; Negative test: vector udiv is exact, scalar is not.
+define i32 @exact_flag_mismatch(<4 x i32> %a, <4 x i32> %b) {
+; CHECK-LABEL: @exact_flag_mismatch(
+; CHECK-NEXT: [[VEC_DIV:%.*]] = udiv exact <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[A1:%.*]] = extractelement <4 x i32> [[A]], i64 1
+; CHECK-NEXT: [[B1:%.*]] = extractelement <4 x i32> [[B]], i64 1
+; CHECK-NEXT: [[SCALAR_DIV:%.*]] = udiv i32 [[A1]], [[B1]]
+; CHECK-NEXT: ret i32 [[SCALAR_DIV]]
+;
+ %vec_div = udiv exact <4 x i32> %a, %b
+ %a1 = extractelement <4 x i32> %a, i64 1
+ %b1 = extractelement <4 x i32> %b, i64 1
+ %scalar_div = udiv i32 %a1, %b1
+ ret i32 %scalar_div
+}
+
+; Negative test: vector binop does NOT dominate scalar (sibling blocks).
+define i32 @non_dominating_block(<4 x i32> %a, <4 x i32> %b, i1 %cond) {
+; CHECK-LABEL: @non_dominating_block(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: br i1 [[COND:%.*]], label [[LEFT:%.*]], label [[RIGHT:%.*]]
+; CHECK: left:
+; CHECK-NEXT: [[VEC_ADD:%.*]] = add <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: [[E:%.*]] = extractelement <4 x i32> [[VEC_ADD]], i64 0
+; CHECK-NEXT: ret i32 [[E]]
+; CHECK: right:
+; CHECK-NEXT: [[A0:%.*]] = extractelement <4 x i32> [[A]], i64 0
+; CHECK-NEXT: [[B0:%.*]] = extractelement <4 x i32> [[B]], i64 0
+; CHECK-NEXT: [[SCALAR_ADD:%.*]] = add i32 [[A0]], [[B0]]
+; CHECK-NEXT: ret i32 [[SCALAR_ADD]]
+;
+entry:
+ br i1 %cond, label %left, label %right
+
+left:
+ %vec_add = add <4 x i32> %a, %b
+ %e = extractelement <4 x i32> %vec_add, i64 0
+ ret i32 %e
+
+right:
+ %a0 = extractelement <4 x i32> %a, i64 0
+ %b0 = extractelement <4 x i32> %b, i64 0
+ %scalar_add = add i32 %a0, %b0
+ ret i32 %scalar_add
+}
+
+; Vector binop in a different (dominating) block.
+define i32 @cross_block(<4 x i32> %a, <4 x i32> %b, i1 %cond) {
+; CHECK-LABEL: @cross_block(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[VEC_ADD:%.*]] = add <4 x i32> [[A:%.*]], [[B:%.*]]
+; CHECK-NEXT: br i1 [[COND:%.*]], label [[THEN:%.*]], label [[ELSE:%.*]]
+; CHECK: then:
+; CHECK-NEXT: [[A3:%.*]] = extractelement <4 x i32> [[A]], i64 3
+; CHECK-NEXT: [[B3:%.*]] = extractelement <4 x i32> [[B]], i64 3
+; CHECK-NEXT: [[TMP0:%.*]] = extractelement <4 x i32> [[VEC_ADD]], i64 3
+; CHECK-NEXT: ret i32 [[TMP0]]
+; CHECK: else:
+; CHECK-NEXT: ret i32 0
+;
+entry:
+ %vec_add = add <4 x i32> %a, %b
+ br i1 %cond, label %then, label %else
+
+then:
+ %a3 = extractelement <4 x i32> %a, i64 3
+ %b3 = extractelement <4 x i32> %b, i64 3
+ %scalar_add = add i32 %a3, %b3
+ ret i32 %scalar_add
+
+else:
+ ret i32 0
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/186102
More information about the llvm-commits
mailing list