blob: b21e33bd44456bba649c07d85f008492a8eb86d2 [file] [view]
# Stack arrays pass
## Problem Description
In gfortran, `-fstack-arrays` will cause all local arrays, including those of
unknown size, to be allocated from stack memory. Gfortran enables this flag by
default at `-Ofast`.
Flang already allocates all local arrays on the stack (the
`memory-allocation-opt` pass can move large allocations to the heap, but by
default it will not). But there are some cases where temporary arrays are
created on the heap by Flang. Moving these temporary arrays is the goal here.
## Proposed Solution
One approach would be to write a pass which attempts to move heap allocations to
the stack (like the `memory-allocation-opt` pass in reverse). This approach has
the advantage of a self-contained implementation and ensuring that newly added
cases are covered.
Another approach would be to modify each place in which arrays are allocated on
the heap to instead generate a stack allocation. The advantage of the second
approach is that it would be difficult to match up all `fir.allocmem` operations
with their associated `fir.freemem`. In general, the lifetimes of heap allocated
memory are not constrained by the current function's stack frame and so cannot
be always converted to stack allocations. It is much easier to swap heap
allocations for stack allocations when they are first generated because the
lifetime information is conveniently available.
The plan is to take the more complex first approach so that newly added changes
to lowering code do not need to be made to support the stack arrays option. The
general problem of determining heap allocation lifetimes can be simplified in
this case because only those allocations which are always freed before exit from
the same function are possible to be moved to heap allocations in that
function's stack frame. Furthermore, the aim of this pass would be to only move
those allocations generated by Flang, and so some of the more difficult cases
can be avoided. Where the transformation is not possible, the existing heap
allocation will be kept.
## Implementation details overview
In order to limit the complexity of the proposed pass, it is useful to
understand the situations in which Flang will generate heap allocations.
### Known Heap Array Allocations
Flang allocates most arrays on the stack by default, but there are a few cases
where temporary arrays are allocated on the heap:
- `flang/lib/Optimizer/Transforms/MemoryAllocation.cpp`
- `flang/lib/Lower/IntrinsicCall.cpp`
- `flang/lib/Lower/ConvertVariable.cpp`
Lowering code is being updated and in the future, temporaries for expressions
will be created in the HLFIR bufferization pass in
`flang/lib/Optimizer/HLFIR/Trnasforms/BufferizeHLFIR.cpp`.
#### `MemoryAllocation.cpp`
The default options for the Memory Allocation transformation ensure that no
array allocations, no matter how large, are moved from the stack to the heap.
#### `IntrinsicCall.cpp`
The existing design is for the runtime to do the allocation and the lowering
code to insert `fir.freemem` to remove the allocation. It is not clear whether
this can be replaced by adapting lowering code to do stack allocations and to
pass these to the runtime. This would be a significant change and so is out of
scope of `-fstack-arrays`.
#### `ConvertVariable.cpp`
See `Fortran::lower::MapSymbolAttributes`:
Dummy arguments that are not declared in the current entry point require a
skeleton definition. Most such "unused" dummies will not survive into final
generated code, but some will. It is illegal to reference one at run time if it
does. There are three ways these dummies can be mapped to a value:
- a `fir::UndefOp` value: preferable but can't be used for dummies with dynamic
bounds or used to define another dummy.
- A stack slot. This has intermediate-weight cost but may not be usable for
objects with dynamic bounds.
- A heap box/descriptor is the heaviest weight option, only used for dynamic
objects.
These heap allocations will be out of scope for `-fstack-arrays` because the
existing implementation already uses stack allocations (or better) where
possible, and most such dummy arguments will be removed from generated code.
#### `BufferizeHLFIR.cpp`
TODO
### Detecting Allocations to Move
Allocations which could be moved to the stack will be detected by performing a
forward dense data flow analysis using `mlir::dataflow::DenseForwardDataFlowAnalysis`.
This analysis will search for SSA values created by a `fir.allocmem` which are
always freed using `fir.freemem` within the same function.
Tracking allocations by SSA values will miss some cases where address
calculations are duplicated in different blocks: resulting in different SSA
values as arguments for the allocation and the free. It is believed that simple
tracking of SSA values is sufficient for detecting the allocations for array
temporaries added by Flang, because these temporaries should be simple arrays:
not nested inside of derived types or other arrays.
Arrays allocated by an `allocate` statement in source code should not be moved
to the stack. These will be identified by adding an attribute to these
`fir.allocmem` operations when they are lowered. Intrinsics such as `allocated`
and `move_alloc` do not need to be supported because the array temporaries moved
to the stack will never be visible in user code.
Only allocations for arrays will be considered for moving to the stack.
When conducting the dense data flow analysis, the pass will assume that existing
allocations are not freed inside called functions. This is true for the existing
heap array temporary allocations generated by Flang. If an allocation were to be
freed inside of a called function, the allocation would presumably not also be
freed later in the caller function (as this would be a double-free). Therefore
the stack arrays pass would not find where the allocation was freed and so not
transform the allocation. It is necessary to make this assumption so that the
stack arrays pass can transform array allocations created for pass-by-value
function arguments.
### Moving Allocations
The `fir.allocmem` will be replaced by a `fir.alloca` with the same arguments.
The `fir.freemem` will be removed entirely.
Where possible, the `fir.alloca` should be placed in the function entry block
(so we can be sure it only happens once). This may not be possible if the array
has non-constant extents (causing the `fir.alloca` to have SSA values as
operands). In this case, the `fir.alloca` will be placed immediately after the
last operand becomes available.
If this location is inside a loop (either an `mlir::LoopLikeOpInterface` or a
cyclic CFG), the transformation should attempt to use the `llvm.stacksave`/
`llvm.stackrestore` intrinsics to ensure that the stack does not grow on every
loop iteration. Use of these intrinsics is considered valid when the allocation
and deallocation occur within the same block and there are no other stack
allocations between them. If this is not the case, the existing heap allocation
will be preserved.
### FIR attribute
A FIR attribute will be added to distinguish `fir.allocmem` arising from
`allocate` statements in source from `fir.allocmem` operations added by Flang.
The attribute will be called `"fir.must_be_heap"` and will have a boolean value:
`true` meaning that the stack arrays pass must not move the allocation, `false`
meaning that stack arrays may move the allocation. Not specifying the attribute
will be equivalent to setting it to `false`.
### Device code
`-fstack-arrays` is not honored in code that runs on an accelerator: CUDA
Fortran `device` and `global` procedures, OpenACC compute constructs
(`acc.parallel`, `acc.kernels`, `acc.serial`) and `cuf.kernel` loops. The
device stack is far smaller than the host one
(1 KB per thread by default on NVIDIA GPUs) and every thread of a launch pays
for the storage, so a runtime-sized array that fits the host stack easily
overflows it. In those contexts runtime-sized allocations stay on the heap. The
size based part of the allocation policy still applies, so small constant size
temporaries may still be placed on the device stack, and fixed size local arrays
are unaffected.
The rule is part of the allocation policy (`flang/Optimizer/Support/
AllocationPolicy.h`), so every stack-or-heap decision applies it:
- Lowering records a `fir.allocation_policy` attribute with `stack_arrays`
disabled on device procedures. `fir::getAllocationPolicy` honors the
innermost policy, so the function attribute narrows the module one.
- `fir::shouldAllocateOnStack` ignores `stack_arrays` when the allocation's
context operation is nested in an offload region (`fir::isInOffloadRegion`).
The `allocation-placement` pass passes the allocation itself as the context;
the `stack-arrays` pass skips such candidates.
An `acc routine` procedure is lowered once for both the host and the device,
so it keeps the host policy. Keeping its device copy off the device stack is
left to the device code generation, which sees the runtime-sized allocations
once the routine has been specialized for the device. Host code, including the
host copy of an `attributes(host,device)` procedure and of an `acc routine`, is
unaffected.
## Testing Plan
FileCheck tests will be written to check each of the above identified sources of
heap allocated array temporaries are detected and converted by the new pass.
Another test will check that `allocate` statements in source code will not be
moved to the stack.
Tests for device code check that allocations inside offload regions and in
device procedures are left on the heap while the same allocation in host code
is moved to the stack.