[flang-commits] [flang] [flang] Extend LICM cheap nested hoisting to scalar loads (PR #217616)

Caroline Newcombe via flang-commits flang-commits at lists.llvm.org
Thu Aug 20 06:21:38 PDT 2026


https://github.com/cenewcombe created https://github.com/llvm/llvm-project/pull/217616

Admit fir.address_of and loads of trivial and descriptor types in the "cheap" nested-region hoisting mode, which previously admitted only fir.convert. This is a cost heuristic change; both modes share `shouldMoveFromNestedRegion`, so the safety conditions are unchanged.

Test added to `flang/test/Transforms/licm.fir` cover the newly added cases (scalar, global scalar, descriptor) and the boundary: derived-type and vector loads are not hoisted, a volatile scalar load stays conditional, and a global scalar read inside an OpenMP worksharing region moves out of the inner serial loop while remaining inside `omp.loop_nest`.

The pass is currently off by default, but if enabled, this patch addresses #110613 and is an intermediate step required for vectorization in #208086.

>From 0b3857b0beab6ba886d8778ab1a62f0d4e875dde Mon Sep 17 00:00:00 2001
From: Caroline Newcombe <caroline.newcombe at hpe.com>
Date: Thu, 20 Aug 2026 07:39:12 -0500
Subject: [PATCH] [flang] Extend LICM cheap nested hoisting to scalar loads

Admit fir.address_of and loads of trivial and descriptor types in the
"cheap" nested-region hoisting mode, which previously admitted only
fir.convert. This is a cost heuristic change; both modes share
shouldMoveFromNestedRegion, so the safety conditions are unchanged.
---
 .../flang/Optimizer/Transforms/Passes.td      |   4 +-
 .../Transforms/LoopInvariantCodeMotion.cpp    |  26 ++-
 flang/test/Transforms/licm.fir                | 208 ++++++++++++++++++
 3 files changed, 233 insertions(+), 5 deletions(-)

diff --git a/flang/include/flang/Optimizer/Transforms/Passes.td b/flang/include/flang/Optimizer/Transforms/Passes.td
index 98090fefeeedc..2303b0453b44d 100644
--- a/flang/include/flang/Optimizer/Transforms/Passes.td
+++ b/flang/include/flang/Optimizer/Transforms/Passes.td
@@ -761,7 +761,9 @@ def LoopInvariantCodeMotion : Pass<"flang-licm", "::mlir::func::FuncOp"> {
             clEnumValN(::fir::LICMNestedHoistingMode::None,
                        "none", "Do not hoist from nested regions"),
             clEnumValN(::fir::LICMNestedHoistingMode::Cheap,
-                       "cheap", "Only hoist cheap ops like fir.convert"),
+                       "cheap", "Only hoist cheap ops like fir.convert, "
+                                "fir.address_of, and scalar or descriptor "
+                                "loads"),
             clEnumValN(::fir::LICMNestedHoistingMode::Aggressive,
                        "aggressive", "Hoist all safe invariant ops")
            )}]>];
diff --git a/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp b/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
index 26388c6758daa..89b11a450e515 100644
--- a/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
+++ b/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
@@ -240,6 +240,26 @@ static bool canHoistLoad(Operation *op, LoopLikeOpInterface loopLike,
   return false;
 }
 
+/// Returns true iff hoisting \p op out of a nested region is expected to be
+/// inexpensive. This is a cost heuristic only; the safety of the hoisting is
+/// established separately.
+///
+/// fir.convert and fir.address_of are at most one instruction and are often
+/// free. A load of a trivial non-vector type is a single access, and a load of
+/// a descriptor is a fixed-size copy. Vector loads may be large, and CHARACTER,
+/// derived types and arrays may be arbitrarily large, so those are left to the
+/// aggressive mode.
+static bool isCheapToHoistFromNestedRegion(Operation *op) {
+  if (isa<fir::ConvertOp, fir::AddrOfOp>(op))
+    return true;
+  if (auto load = dyn_cast<fir::LoadOp>(op)) {
+    Type resultType = load.getType();
+    return isa<fir::BaseBoxType>(resultType) ||
+           (fir::isa_trivial(resultType) && !fir::isa_vector(resultType));
+  }
+  return false;
+}
+
 /// Recursively collect regions from operations inside \p region, skipping
 /// IsolatedFromAbove operations (whose regions form a separate scope) and
 /// LoopLikeOpInterface operations (which have their own LICM invocation).
@@ -437,14 +457,12 @@ void LoopInvariantCodeMotion::runOnOperation() {
       moveLoopInvariantCode(nestedRegions, isDefinedOutsideRegion,
                             shouldMoveFromNestedRegion, moveOutOfRegion);
     } else {
-      // "cheap" mode: only hoist fir.convert.
-      // TODO: refine the cost model for "cheap" hoisting to include
-      // other inexpensive operations.
+      // "cheap" mode: only hoist operations that are inexpensive to move.
       moveLoopInvariantCode(
           nestedRegions, isDefinedOutsideRegion,
           /*shouldMoveOutOfRegion=*/
           [&](Operation *op, Region *region) {
-            return isa<fir::ConvertOp>(op) &&
+            return isCheapToHoistFromNestedRegion(op) &&
                    shouldMoveFromNestedRegion(op, region);
           },
           moveOutOfRegion);
diff --git a/flang/test/Transforms/licm.fir b/flang/test/Transforms/licm.fir
index 5b49934a5e7ad..abe0ac8cfdefc 100644
--- a/flang/test/Transforms/licm.fir
+++ b/flang/test/Transforms/licm.fir
@@ -2689,3 +2689,211 @@ func.func @test_hoist_hlfir_null(%arg0: !fir.ref<i64>) {
   }
   return
 }
+
+// -----
+// Test that a scalar load inside scf.if is hoisted in the default "cheap"
+// nested hoisting mode, along with the fir.convert chain depending on it.
+// CHECK-LABEL:   func.func @test_cheap_nested_scalar_load(
+// CHECK-SAME:      %[[ARG0:.*]]: !fir.ref<!fir.array<10xf32>>,
+// CHECK-SAME:      %[[ARG1:.*]]: !fir.ref<i32>
+// CHECK:           %[[DECL:.*]] = fir.declare %[[ARG1]]
+// CHECK:           %[[CVT:.*]] = fir.convert %[[ARG0]]
+// CHECK:           %[[LOAD:.*]] = fir.load %[[DECL]]
+// CHECK:           %[[IDX:.*]] = fir.convert %[[LOAD]] : (i32) -> index
+// CHECK:           scf.for
+// CHECK:             scf.if
+// CHECK-NOT:           fir.load
+// CHECK:               memref.store %{{.*}}, %[[CVT]]{{\[}}%[[IDX]]{{\]}}
+func.func @test_cheap_nested_scalar_load(%arg0: !fir.ref<!fir.array<10xf32>>, %arg1: !fir.ref<i32>) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c5 = arith.constant 5 : index
+  %c10 = arith.constant 10 : index
+  %cst = arith.constant 1.000000e+00 : f32
+  %0 = fir.dummy_scope : !fir.dscope
+  %1 = fir.declare %arg1 dummy_scope %0 arg 2 {uniq_name = "_QFtestEn"} : (!fir.ref<i32>, !fir.dscope) -> !fir.ref<i32>
+  scf.for %i = %c0 to %c10 step %c1 {
+    %cond = arith.cmpi slt, %i, %c5 : index
+    scf.if %cond {
+      %cvt = fir.convert %arg0 : (!fir.ref<!fir.array<10xf32>>) -> memref<10xf32>
+      %val = fir.load %1 : !fir.ref<i32>
+      %idx = fir.convert %val : (i32) -> index
+      memref.store %cst, %cvt[%idx] : memref<10xf32>
+    }
+  }
+  return
+}
+
+// -----
+// Test that a load of a global scalar inside scf.if is hoisted in the default
+// "cheap" nested hoisting mode.
+// CHECK-LABEL:   func.func @test_cheap_nested_global_scalar_load(
+// CHECK:           %[[ADDR:.*]] = fir.address_of(@global_scalar)
+// CHECK:           %[[LOAD:.*]] = fir.load %[[ADDR]]
+// CHECK:           scf.for
+// CHECK:             scf.if
+// CHECK-NOT:           fir.load
+// CHECK:               fir.store %[[LOAD]]
+fir.global @global_scalar : f32
+
+func.func @test_cheap_nested_global_scalar_load(%arg0: !fir.ref<f32>) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c5 = arith.constant 5 : index
+  %c10 = arith.constant 10 : index
+  scf.for %i = %c0 to %c10 step %c1 {
+    %cond = arith.cmpi slt, %i, %c5 : index
+    scf.if %cond {
+      %addr = fir.address_of(@global_scalar) : !fir.ref<f32>
+      %val = fir.load %addr : !fir.ref<f32>
+      fir.store %val to %arg0 : !fir.ref<f32>
+    }
+  }
+  return
+}
+
+// -----
+// Test that a descriptor load inside scf.if is hoisted in the default "cheap"
+// nested hoisting mode. A descriptor load is a fixed-size copy.
+// CHECK-LABEL:   func.func @test_cheap_nested_descriptor_load(
+// CHECK-SAME:      %[[ARG0:.*]]: !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
+// CHECK:           %[[BOX:.*]] = fir.load %[[ARG0]]
+// CHECK:           scf.for
+// CHECK:             scf.if
+// CHECK-NOT:           fir.load %[[ARG0]]
+func.func @test_cheap_nested_descriptor_load(%arg0: !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>, %arg1: !fir.ref<f32>) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c5 = arith.constant 5 : index
+  %c10 = arith.constant 10 : index
+  scf.for %i = %c0 to %c10 step %c1 {
+    %cond = arith.cmpi slt, %i, %c5 : index
+    scf.if %cond {
+      %box = fir.load %arg0 : !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
+      %addr = fir.box_addr %box : (!fir.box<!fir.heap<!fir.array<?xf32>>>) -> !fir.heap<!fir.array<?xf32>>
+      %elt = fir.coordinate_of %addr, %c0 : (!fir.heap<!fir.array<?xf32>>, index) -> !fir.ref<f32>
+      %val = fir.load %elt : !fir.ref<f32>
+      fir.store %val to %arg1 : !fir.ref<f32>
+    }
+  }
+  return
+}
+
+// -----
+// Test that a load of a derived type inside scf.if is NOT hoisted in the
+// default "cheap" nested hoisting mode, because it may be arbitrarily large.
+// CHECK-LABEL:   func.func @test_cheap_nested_skips_derived_load(
+// CHECK:           scf.for
+// CHECK:             scf.if
+// CHECK:               fir.load
+func.func @test_cheap_nested_skips_derived_load(%arg0: !fir.ref<!fir.type<t{a:i32,b:i32}>>, %arg1: !fir.ref<!fir.type<t{a:i32,b:i32}>>) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c5 = arith.constant 5 : index
+  %c10 = arith.constant 10 : index
+  scf.for %i = %c0 to %c10 step %c1 {
+    %cond = arith.cmpi slt, %i, %c5 : index
+    scf.if %cond {
+      %val = fir.load %arg0 : !fir.ref<!fir.type<t{a:i32,b:i32}>>
+      fir.store %val to %arg1 : !fir.ref<!fir.type<t{a:i32,b:i32}>>
+    }
+  }
+  return
+}
+
+// -----
+// Test that a vector load inside scf.if is NOT hoisted in the default "cheap"
+// nested hoisting mode. Vector loads may be large and are excluded from cheap
+// mode.
+// CHECK-LABEL:   func.func @test_cheap_nested_skips_vector_load(
+// CHECK:           scf.for
+// CHECK:             scf.if
+// CHECK:               fir.load
+// CHECK:               fir.store
+func.func @test_cheap_nested_skips_vector_load(%arg0: !fir.ref<!fir.vector<8:f64>>, %arg1: !fir.ref<!fir.vector<8:f64>>) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c5 = arith.constant 5 : index
+  %c10 = arith.constant 10 : index
+  scf.for %i = %c0 to %c10 step %c1 {
+    %cond = arith.cmpi slt, %i, %c5 : index
+    scf.if %cond {
+      %val = fir.load %arg0 : !fir.ref<!fir.vector<8:f64>>
+      fir.store %val to %arg1 : !fir.ref<!fir.vector<8:f64>>
+    }
+  }
+  return
+}
+
+// -----
+// Test that a volatile scalar load inside scf.if is NOT hoisted in the default
+// "cheap" nested hoisting mode. The cost model admits it as a trivial scalar;
+// the safety path rejects it.
+// CHECK-LABEL:   func.func @test_cheap_nested_volatile_load(
+// CHECK:           scf.for
+// CHECK:             scf.if
+// CHECK:               fir.load{{.*}}volatile
+// CHECK:               fir.store
+func.func @test_cheap_nested_volatile_load(%arg0: !fir.ref<i32>, %arg1: !fir.ref<i32>) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %c5 = arith.constant 5 : index
+  %c10 = arith.constant 10 : index
+  %0 = fir.dummy_scope : !fir.dscope
+  %1 = fir.volatile_cast %arg0 : (!fir.ref<i32>) -> !fir.ref<i32, volatile>
+  %2 = fir.declare %1 dummy_scope %0 arg 1 {fortran_attrs = #fir.var_attrs<volatile>, uniq_name = "_QFtest_cheap_nested_volatile_loadEv"} : (!fir.ref<i32, volatile>, !fir.dscope) -> !fir.ref<i32, volatile>
+  scf.for %i = %c0 to %c10 step %c1 {
+    %cond = arith.cmpi slt, %i, %c5 : index
+    scf.if %cond {
+      %val = fir.load %2 : !fir.ref<i32, volatile>
+      fir.store %val to %arg1 : !fir.ref<i32>
+    }
+  }
+  return
+}
+
+// -----
+// Test the shape that motivates the change: a global scalar read under a guard
+// in a serial loop nested inside an OpenMP worksharing region. The load must
+// move out of the inner fir.do_loop but stay inside omp.loop_nest, since
+// omp.wsloop requires tight nesting.
+// CHECK-LABEL:   func.func @test_cheap_nested_omp_global_scalar(
+// CHECK:           omp.parallel
+// CHECK:             omp.wsloop
+// CHECK:               omp.loop_nest
+// CHECK:                 %[[ADDR:.*]] = fir.address_of(@omp_global_scalar)
+// CHECK:                 %[[LOAD:.*]] = fir.load %[[ADDR]]
+// CHECK:                 fir.do_loop
+// CHECK:                   scf.if
+// CHECK-NOT:                 fir.load
+// CHECK:                     fir.store %[[LOAD]]
+fir.global @omp_global_scalar : f64
+
+omp.private {type = private} @_QFtest_ompEi_private_i32 : i32
+
+func.func @test_cheap_nested_omp_global_scalar(%arg0: !fir.ref<f64>) {
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  %c1 = arith.constant 1 : index
+  %c2 = arith.constant 2 : index
+  %c3 = arith.constant 3 : index
+  %0 = fir.alloca i32 {bindc_name = "i", uniq_name = "_QFtest_ompEi"}
+  %1 = fir.declare %0 {uniq_name = "_QFtest_ompEi"} : (!fir.ref<i32>) -> !fir.ref<i32>
+  omp.parallel {
+    omp.wsloop private(@_QFtest_ompEi_private_i32 %1 -> %arg1 : !fir.ref<i32>) {
+      omp.loop_nest (%arg2) : i32 = (%c1_i32) to (%c10_i32) inclusive step (%c1_i32) {
+        fir.do_loop %k = %c1 to %c3 step %c1 {
+          %cond = arith.cmpi slt, %k, %c2 : index
+          scf.if %cond {
+            %addr = fir.address_of(@omp_global_scalar) : !fir.ref<f64>
+            %val = fir.load %addr : !fir.ref<f64>
+            fir.store %val to %arg0 : !fir.ref<f64>
+          }
+        }
+        omp.yield
+      }
+    }
+    omp.terminator
+  }
+  return
+}



More information about the flang-commits mailing list