[llvm] [SCEV] Fix scMulExpr cost for multiply by -1 and power-of-2 (PR #191033)

Adel Ejjeh via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 8 12:09:35 PDT 2026


https://github.com/adelejjeh created https://github.com/llvm/llvm-project/pull/191033

## Summary

The SCEV expansion cost model in `costAndCollectOperands` always asks
`TTI.getArithmeticInstrCost(Mul, ...)` for `scMulExpr`, but
`SCEVExpander::visitMulExpr` — the code that actually generates IR
from the SCEV expression — never emits a multiply for two common
cases:

- `-1 * X` → generates `sub 0, X`
- `2^k * X` → generates `shl X, k`

This is target-independent: the expander unconditionally checks for
all-ones and power-of-2 constants before falling back to `mul`.

The mismatch matters on targets where integer multiply is expensive.
On AMDGPU, `i32 mul` has TTI cost 4. A trivial trip count like
`(-1 * start) + end` from `for (i = start; i < end; i++)` gets priced
at 4 (mul) + 1 (add) = 5, exceeding the default
`SCEVCheapExpansionBudget` of 4, and runtime loop unrolling is
rejected. The actual expansion cost is 1 (sub) + 1 (add) = 2.

This patch checks the constant operand in two-operand `scMulExpr` and
costs it as the operation the expander will generate. This is the same
approach already used for `scUDivExpr`, which costs power-of-2
division as `LShr` (lines directly above the changed code).

The general case (non-constant multipliers, >2 operands, binpow) is
left as-is; the existing TODO is preserved.

## Changes

- **`llvm/lib/Transforms/Utils/ScalarEvolutionExpander.cpp`** — In
  `costAndCollectOperands`, for two-operand `scMulExpr` with a
  `SCEVConstant` LHS: cost all-ones (-1) as `Instruction::Sub` and
  power-of-2 as `Instruction::Shl` instead of `Instruction::Mul`.

- **`llvm/test/Transforms/LoopUnroll/scev-mul-expansion-cost.ll`**
  *(new)* — Verifies an AMDGPU offset-start loop
  (`for (i = start; i < end; i++)`) is runtime-unrolled 8x with the
  default expansion budget. Uses AMDGPU triple because its i32 mul
  cost (4) makes the over-counting observable against the default
  budget (4).

Assisted-by: Cursor (Claude)

Made with [Cursor](https://cursor.com)

>From 2edc5e05b26b4fe69ea11d4ab200e824b8ffc4e2 Mon Sep 17 00:00:00 2001
From: Adel Ejjeh <adel.ejjeh at amd.com>
Date: Wed, 8 Apr 2026 14:06:11 -0500
Subject: [PATCH] [SCEV] Fix scMulExpr cost for multiply by -1 and power-of-2
 Assisted-by: Cursor (Claude)

---
 .../Utils/ScalarEvolutionExpander.cpp         | 24 ++++++++---
 .../LoopUnroll/scev-mul-expansion-cost.ll     | 40 +++++++++++++++++++
 2 files changed, 59 insertions(+), 5 deletions(-)
 create mode 100644 llvm/test/Transforms/LoopUnroll/scev-mul-expansion-cost.ll

diff --git a/llvm/lib/Transforms/Utils/ScalarEvolutionExpander.cpp b/llvm/lib/Transforms/Utils/ScalarEvolutionExpander.cpp
index a560c324b4f1e..5f2468f54e2fd 100644
--- a/llvm/lib/Transforms/Utils/ScalarEvolutionExpander.cpp
+++ b/llvm/lib/Transforms/Utils/ScalarEvolutionExpander.cpp
@@ -2023,12 +2023,26 @@ template<typename T> static InstructionCost costAndCollectOperands(
   case scAddExpr:
     Cost = ArithCost(Instruction::Add, S->getNumOperands() - 1);
     break;
-  case scMulExpr:
-    // TODO: this is a very pessimistic cost modelling for Mul,
-    // because of Bin Pow algorithm actually used by the expander,
-    // see SCEVExpander::visitMulExpr(), ExpandOpBinPowN().
-    Cost = ArithCost(Instruction::Mul, S->getNumOperands() - 1);
+  case scMulExpr: {
+    // Match the actual expansion in visitMulExpr: multiply by -1 is
+    // expanded as a negate (sub 0, x), and multiply by a power of 2 is
+    // expanded as a shift.  Only handle the common two-operand case with a
+    // constant LHS; for everything else fall back to the pessimistic
+    // all-multiplies estimate.
+    // TODO: this is still pessimistic for the general case because of the
+    // Bin Pow algorithm actually used by the expander, see
+    // SCEVExpander::visitMulExpr(), ExpandOpBinPowN().
+    unsigned MulOpc = Instruction::Mul;
+    if (S->getNumOperands() == 2)
+      if (auto *SC = dyn_cast<SCEVConstant>(S->getOperand(0))) {
+        if (SC->getAPInt().isAllOnes()) // -1
+          MulOpc = Instruction::Sub;
+        else if (SC->getAPInt().isPowerOf2())
+          MulOpc = Instruction::Shl;
+      }
+    Cost = ArithCost(MulOpc, S->getNumOperands() - 1);
     break;
+  }
   case scSMaxExpr:
   case scUMaxExpr:
   case scSMinExpr:
diff --git a/llvm/test/Transforms/LoopUnroll/scev-mul-expansion-cost.ll b/llvm/test/Transforms/LoopUnroll/scev-mul-expansion-cost.ll
new file mode 100644
index 0000000000000..8e188f280fdf3
--- /dev/null
+++ b/llvm/test/Transforms/LoopUnroll/scev-mul-expansion-cost.ll
@@ -0,0 +1,40 @@
+; RUN: opt -S -passes=loop-unroll -unroll-runtime < %s | FileCheck %s
+
+; The trip count SCEV for "for (i = start; i < end; i++)" is
+; (-1 * start) + end.  The SCEV expansion cost model should recognize that
+; (-1 * X) is expanded as a negate (sub 0, X), not a real multiply.  On
+; targets where i32 mul is expensive (e.g. AMDGPU quarter-rate), the old
+; cost model would over-count this as a mul (cost 4) exceeding the default
+; budget of 4, and reject runtime unrolling.  With the fix, it's costed as
+; a sub (cost 1), well within budget.
+
+; AMDGPU is used because its i32 mul has a cost of 4,
+; making the over-counting observable against the default budget of 4.
+target datalayout = "e-p:64:64-p1:64:64-p2:32:32-p3:32:32-p4:64:64-p5:32:32-p6:32:32-p7:160:256:256:32-p8:128:128-p9:192:256:256:32-ni:7:8:9"
+target triple = "amdgcn-amd-amdhsa"
+
+; CHECK-LABEL: @offset_start_loop
+; The loop should be runtime-unrolled 8x (prologue + main unrolled body).
+; CHECK: %xtraiter = and i32 %{{.*}}, 7
+; CHECK: loop.prol:
+; CHECK: loop:
+; CHECK: %iv.next.7 = add nsw i32 %iv, 8
+define void @offset_start_loop(ptr addrspace(1) %out, ptr addrspace(1) %in, i32 %start, i32 %end) {
+entry:
+  %cmp = icmp slt i32 %start, %end
+  br i1 %cmp, label %loop, label %exit
+
+loop:
+  %iv = phi i32 [ %start, %entry ], [ %iv.next, %loop ]
+  %idx = sext i32 %iv to i64
+  %gep.in = getelementptr inbounds float, ptr addrspace(1) %in, i64 %idx
+  %val = load float, ptr addrspace(1) %gep.in, align 4
+  %gep.out = getelementptr inbounds float, ptr addrspace(1) %out, i64 %idx
+  store float %val, ptr addrspace(1) %gep.out, align 4
+  %iv.next = add nsw i32 %iv, 1
+  %cond = icmp slt i32 %iv.next, %end
+  br i1 %cond, label %loop, label %exit
+
+exit:
+  ret void
+}



More information about the llvm-commits mailing list