[RFC][Flang] Improving temporary and local array allocations - unify StackArray/MemoryAllocation

Flang currently splits the stack-vs-heap decision for arrays across two opposing,
mutually exclusive cleanup passes: stack-arrays (moves heap temporaries to the
stack, run only under -fstack-arrays) and memory-allocation-opt (moves stack
allocations to the heap, effectively a no-op in the default pipeline). Because
they run in isolation and pull in opposite directions, the default behaviour is
hard to reason about and to tune. Two defaults are notably off: every automatic
(runtime-sized) array is stack-allocated — risking stack overflow for large or
recursive cases — while every compiler temporary emitted by HLFIR lowering is
heap-allocated, adding malloc/free overhead even for tiny temporaries.

This RFC proposes replacing both passes with a single, policy-driven
allocation-placement pass that classifies each allocation (user variable vs.
temporary; runtime vs. constant size; small vs. large) and places it
accordingly, with tunable thresholds and a hook for target/region-specific
policies. Relative to today, the default (-fno-stack-arrays) policy changes two
behaviours: automatic/runtime-sized arrays go on the heap (matching gfortran
and nvfortran, avoiding stack overflow), and small constant-size temporaries go
on the stack (avoiding needless heap traffic). -fstack-arrays keeps its
meaning: place everything on the stack, best-effort.

Existing compiler behaviour (S = stack, H = heap)

Columns: User = named local array, Temp = compiler temporary; dyn = runtime
size, small/big = small/large constant size.

Compiler / mode User dyn User small User big Temp dyn Temp small Temp big
gfortran H S S S
gfortran -fstack-arrays S S S S S
nvfortran H S S H
nvfortran -Mstack_arrays S S S H
ifx S S S S S S
ifx -heap-arrays H S S H H H
flang (today) S S S H H H
flang (today) -fstack-arrays S S S S S S
flang (proposed) H S S H S H
flang (proposed) -fstack-arrays S S S S S S

¹ gfortran heaps array-constructor temporaries of runtime size (even with
-fstack-arrays); cells show elementwise expression temporaries.
² gfortran heaps constant-size temporaries larger than ~64 KB
(-fmax-stack-var-size).
³ nvfortran keeps constant-size array-constructor temporaries on the stack;
cells show elementwise expression temporaries.

Implementation

This RFC is implemented by the following stack of LLVM PRs (merge in order):

  1. #210721 — [flang][NFC] Extract StackArrays analysis and rewrite into a header
  2. #210742 — [flang] Add policy-driven allocation-placement pass
  3. #210745 — [flang] Wire allocation-placement into the optimizer pipeline (default off)
  4. #210766 — [flang] Enable allocation-placement pass by default
  5. #210930 — [flang] Remove legacy stack-arrays and memory-allocation-opt passes

There are no functional changes to flang existsing behavior until step 4 that enables the new pass by default.

3 Likes

This looks good to me in principle and is frequently requested by users.

It is unlikely, but please could you check for any serious benchmark regressions in the default pipeline? Users wanting peak performance should use -fstack-arrays, and it sounds like that won’t change, so I wouldn’t want to block this over a small regression. But I would prefer to avoid a large regression in the default pipeline.

Thanks for the feedback @tblah

Yes, -fstack-array behavior is unchanged and I agree it is the reference used when people run benchmarks/wants the best CPU perf.

I did a few runs with -O3 -ffast-math -fno-protect-parens -march=native (equivalent to -Ofast -fno-stack-arrays) on an AMD EPYC 9334, and there are a some changes in both directions.

The main impact is a big slow down on WRF that slows down from 123s to 255s at -fno-stack-arrays. You can also see that Polyhedron nf_11 and Spec bwaves gets some slow down with the new -fno-stack-array. This is all due to the move of automatic array on the heap. On the contrary, tonto from Spec gets some speed-up thanks to the move of small constant size array temporaries on the stack (closer perf to -fstack-arrays). So the proposed change will have some impact -fno-stack-arrays per in both directions. Overall, I think it is a net usability win for non HPC users to not have to use unlimited stack by default, and it looks like an acceptable trade-off for the HPC users of -fno-stack-arrays.

Polyhedron Curent -fno-stack-arrays Proposed -fno-stack-arrays
ac_11 4.425 4.426
aermod_11 3.931 3.817
air_11 1.535 1.527
capacita_11 12.64 12.61
channel2_11 25.35 25.38
doduc_11 5.237 5.19
fatigue2_11 32.79 32.82
gas_dyn2_11 24.34 24.47
induct2_11 17.45 17.46
linpk_11 1.735 1.783
mdbx_11 3.987 3.955
mp_prop_design_11 40.38 40.46
nf_11 3.153 4.389
protein_11 10.44 10.34
rnflow_11 9.267 9.298
tfft2_11 15.52 15.36
SPEC FP2006
410.bwaves(FTN) 153.2 160.6
416.gamess(FTN) 289.1 288.3
434.zeusmp(FTN) 82.69 83.59
437.leslie3d(FTN) 84.3 84.86
459.GemsFDTD(FTN) 138.5 138.2
465.tonto(FTN) 138.9 129.1
481.wrf(FTN,C) 124 255.23
SPEC FP2000
168.wupwise(FTN) 24.1 24.14
171.swim(FTN) 20.86 20.9
172.mgrid(FTN) 17.42 17.46
173.applu(FTN) 36.91 36.82
178.galgel(FTN) 11.76 11.72
187.facerec(FTN) 14.18 14.57
189.lucas(FTN) 14.32 14.31
191.fma3d(FTN) 23.14 23.39
301.apsi(FTN) 24.6 24.89

Note that WRF is already noticeably faster with -fstack-arrays than it is with -fno-stack-arrays (98s vs 123s) and the -fstack-arrays results are unchanged.

Thank you for running your benchmarks.

Here are some numbers of mine for wrf_r in spec2017 (I had that more easily to hand than 2006) on an aarch64 machine.

Compiler -fstack-arrays Runtime vs base Flang Effect of -fstack-arrays
Base Flang 11640c8923e0 No Baseline
RFC Flang cbf4a631fdbc No 3.8% slower
gfortran 13.3.0 No 2.3% slower
Base Flang 11640c8923e0 Yes Baseline Unchanged vs baseline
RFC Flang cbf4a631fdbc Yes Unchanged vs baseline 3.7% faster
gfortran 13.3.0 Yes 0.9% faster 3.1% faster

I was interested in how gfortran compares as another compiler which puts automatic arrays on the heap. It is hard to extrapolate from this one result but it seems plausible that a worse result is inherent in using the heap for array temporaries on this benchmark.

These numbers look much less concerning than specfp2006 wrf. I wonder if something could be improved for that particular benchmark to make the impact less severe.

If my AI didn’t hallucinate, given your numbers: SPECFP2000 shows negligible change in geomean, polyhedron is 1.88% worse (mostly because of linkpk_11); but the spec2006 regression is quite bad. Over all three suites, if spec2006 wrf is excluded, the regression is only 1.08%.

I think something on the order of of 1% overall is acceptable if either

  1. We find a way to improve the 106% regression in spec2006 wrf
  2. Or if it turns out every compiler you listed other than llvm-flang (without extra flags) scores similarly on spec2006 wrf

(And similar if we discover other benchmarks with massive regressions like wrf).

I am open to other suggestions on how to address this without dramatically worsening flang by default.

Given the risk of regressions it would also be helpful if you include driver flags to emulate the old default.