Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[fir] Add array operations documentation
This patch adds documentation on FIR array operations and their usage. Reviewed By: schweitz Differential Revision: https://reviews.llvm.org/D115077
- Loading branch information
1 parent
99b5a80
commit 69825f3
Showing
1 changed file
with
342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
<!--===- docs/FIRArrayOperations.md | ||
Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | ||
See https://llvm.org/LICENSE.txt for license information. | ||
SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | ||
--> | ||
|
||
# Design: FIR Array operations | ||
|
||
```eval_rst | ||
.. contents:: | ||
:local: | ||
``` | ||
|
||
## General | ||
|
||
The array operations in FIR model the copy-in/copy-out semantics over Fortran | ||
statements. | ||
|
||
Fortran language semantics sometimes require the compiler to make a temporary | ||
copy of an array or array slice. Situations where this can occur include: | ||
|
||
* Passing a non-contiguous array to a procedure that does not declare it as | ||
assumed-shape. | ||
* Array expressions, especially those involving `RESHAPE`, `PACK`, and `MERGE`. | ||
* Assignments of arrays where the array appears on both the left and right-hand | ||
sides of the assignment. | ||
* Assignments of `POINTER` arrays. | ||
|
||
There are currently the following operations: | ||
- `fir.array_load` | ||
- `fir.array_merge_store` | ||
- `fir.array_fetch` | ||
- `fir.array_update` | ||
- `fir.array_access` | ||
- `fir.array_amend` | ||
|
||
`array_load`(s) and `array_merge_store` are a pairing that brackets the lifetime | ||
of the array copies. | ||
|
||
`array_fetch` and `array_update` are defined to work as getter/setter pairs on | ||
values of elements from loaded array copies. These have "GEP-like" syntax and | ||
semantics. | ||
|
||
Fortran arrays are implicitly memory bound as are some other Fortran type/kind | ||
entities. For entities that can be atomically promoted to the value domain, | ||
we use `array_fetch` and `array_update`. | ||
|
||
`array_access` and `array_amend` are defined to work as getter/setter pairs on | ||
references to elements in loaded array copies. `array_access` has "GEP-like" | ||
syntax. `array_amend` annotates which loaded array copy is being written to. | ||
It is invalid to update an array copy without `array_amend`; doing so will | ||
result in undefined behavior. | ||
For those type/kinds that cannot be promoted to values, we must leave them in a | ||
memory reference domain, and we use `array_access` and `array_amend`. | ||
|
||
## array_load | ||
|
||
This operation taken with `array_merge_store` captures Fortran's | ||
copy-in/copy-out semantics. One way to think of this is that array_load | ||
creates a snapshot copy of the entire array. This copy can then be used | ||
as the "original value" of the array while the array's new value is | ||
computed. The `array_merge_store` operation is the copy-out semantics, which | ||
merge the updates with the original array value to produce the final array | ||
result. This abstracts the copy operations as opposed to always creating | ||
copies or requiring dependence analysis be performed on the syntax trees | ||
and before lowering to the IR. | ||
|
||
Load an entire array as a single SSA value. | ||
|
||
```fortran | ||
real :: a(o:n,p:m) | ||
... | ||
... = ... a ... | ||
``` | ||
|
||
One can use `fir.array_load` to produce an ssa-value that captures an | ||
immutable value of the entire array `a`, as in the Fortran array expression | ||
shown above. Subsequent changes to the memory containing the array do not | ||
alter its composite value. This operation lets one load an array as a | ||
value while applying a runtime shape, shift, or slice to the memory | ||
reference, and its semantics guarantee immutability. | ||
|
||
```mlir | ||
%s = fir.shape_shift %lb1, %ex1, %lb2, %ex2 : (index, index, index, index) -> !fir.shape<2> | ||
// load the entire array 'a' | ||
%v = fir.array_load %a(%s) : (!fir.ref<!fir.array<?x?xf32>>, !fir.shape<2>) -> !fir.array<?x?xf32> | ||
// a fir.store here into array %a does not change %v | ||
``` | ||
|
||
# array_merge_store | ||
|
||
The `array_merge_store` operation stores a merged array value to memory. | ||
|
||
|
||
```fortran | ||
real :: a(n,m) | ||
... | ||
a = ... | ||
``` | ||
|
||
One can use `fir.array_merge_store` to merge/copy the value of `a` in an | ||
array expression as shown above. | ||
|
||
```mlir | ||
%v = fir.array_load %a(%shape) : ... | ||
%r = fir.array_update %v, %f, %i, %j : (!fir.array<?x?xf32>, f32, index, index) -> !fir.array<?x?xf32> | ||
fir.array_merge_store %v, %r to %a : !fir.ref<!fir.array<?x?xf32>> | ||
``` | ||
|
||
This operation merges the original loaded array value, `%v`, with the | ||
chained updates, `%r`, and stores the result to the array at address, `%a`. | ||
|
||
This operation taken with `array_load`'s captures Fortran's | ||
copy-in/copy-out semantics. The first operands of `array_merge_store` is the | ||
result of the initial `array_load` operation. While this value could be | ||
retrieved by reference chasiing through the different array operations it is | ||
useful to have it on hand directly for analysis passes since this directly | ||
defines the "bounds" of the Fortran statement represented by these operations. | ||
The intention is to allow copy-in/copy-out regions to be easily delineated, | ||
analyzed, and optimized. | ||
|
||
## array_fetch | ||
|
||
The `array_fetch` operation fetches the value of an element in an array value. | ||
|
||
```fortran | ||
real :: a(n,m) | ||
... | ||
... a ... | ||
... a(r,s+1) ... | ||
``` | ||
|
||
One can use `fir.array_fetch` to fetch the (implied) value of `a(i,j)` in | ||
an array expression as shown above. It can also be used to extract the | ||
element `a(r,s+1)` in the second expression. | ||
|
||
```mlir | ||
%s = fir.shape %n, %m : (index, index) -> !fir.shape<2> | ||
// load the entire array 'a' | ||
%v = fir.array_load %a(%s) : (!fir.ref<!fir.array<?x?xf32>>, !fir.shape<2>) -> !fir.array<?x?xf32> | ||
// fetch the value of one of the array value's elements | ||
%1 = fir.array_fetch %v, %i, %j : (!fir.array<?x?xf32>, index, index) -> f32 | ||
``` | ||
|
||
It is only possible to use `array_fetch` on an `array_load` result value or a | ||
value that can be trace back transitively to an `array_load` as the dominating | ||
source. Other array operation such as `array_update` can be in between. | ||
|
||
## array_update | ||
|
||
The `array_update` operation is used to update the value of an element in an | ||
array value. A new array value is returned where all element values of the input | ||
array are identical except for the selected element which is the value passed in | ||
the update. | ||
|
||
```fortran | ||
real :: a(n,m) | ||
... | ||
a = ... | ||
``` | ||
|
||
One can use `fir.array_update` to update the (implied) value of `a(i,j)` | ||
in an array expression as shown above. | ||
|
||
```mlir | ||
%s = fir.shape %n, %m : (index, index) -> !fir.shape<2> | ||
// load the entire array 'a' | ||
%v = fir.array_load %a(%s) : (!fir.ref<!fir.array<?x?xf32>>, !fir.shape<2>) -> !fir.array<?x?xf32> | ||
// update the value of one of the array value's elements | ||
// %r_{ij} = %f if (i,j) = (%i,%j), %v_{ij} otherwise | ||
%r = fir.array_update %v, %f, %i, %j : (!fir.array<?x?xf32>, f32, index, index) -> !fir.array<?x?xf32> | ||
fir.array_merge_store %v, %r to %a : !fir.ref<!fir.array<?x?xf32>> | ||
``` | ||
|
||
An array value update behaves as if a mapping function from the indices | ||
to the new value has been added, replacing the previous mapping. These | ||
mappings can be added to the ssa-value, but will not be materialized in | ||
memory until the `fir.array_merge_store` is performed. | ||
`fir.array_update` can be seen as an array access with a notion that the array | ||
will be changed at the accessed position when `fir.array_merge_store` is | ||
performed. | ||
|
||
## array_access | ||
|
||
The `array_access` provides a reference to a single element from an array value. | ||
This is *not* a view in the immutable array, otherwise it couldn't be stored to. | ||
It can be see as a logical copy of the element and its position in the array. | ||
Tis reference can be written to and modified withoiut changing the original | ||
array. | ||
|
||
The `array_access` operation is used to fetch the memory reference of an element | ||
in an array value. | ||
|
||
```fortran | ||
real :: a(n,m) | ||
... | ||
... a ... | ||
... a(r,s+1) ... | ||
``` | ||
|
||
One can use `fir.array_access` to recover the implied memory reference to | ||
the element `a(i,j)` in an array expression `a` as shown above. It can also | ||
be used to recover the reference element `a(r,s+1)` in the second | ||
expression. | ||
|
||
```mlir | ||
%s = fir.shape %n, %m : (index, index) -> !fir.shape<2> | ||
// load the entire array 'a' | ||
%v = fir.array_load %a(%s) : (!fir.ref<!fir.array<?x?xf32>>, !fir.shape<2>) -> !fir.array<?x?xf32> | ||
// fetch the value of one of the array value's elements | ||
%1 = fir.array_access %v, %i, %j : (!fir.array<?x?xf32>, index, index) -> !fir.ref<f32> | ||
``` | ||
|
||
It is only possible to use `array_access` on an `array_load` result value or a | ||
value that can be trace back transitively to an `array_load` as the dominating | ||
source. Other array operation such as `array_amend` can be in between. | ||
|
||
`array_access` if mainly used with `character`'s arrays and arrays of derived | ||
types where because they might have a non-compile time sizes that would be | ||
useless too load entirely or too big to load. | ||
|
||
Here is a simple example with a `character` array assignment. | ||
|
||
Fortran | ||
``` | ||
subroutine foo(c1, c2, n) | ||
integer(8) :: n | ||
character(n) :: c1(:), c2(:) | ||
c1 = c2 | ||
end subroutine | ||
``` | ||
|
||
It results in this cleaned-up FIR: | ||
``` | ||
func @_QPfoo(%arg0: !fir.box<!fir.array<?x!fir.char<1,?>>>, %arg1: !fir.box<!fir.array<?x!fir.char<1,?>>>, %arg2: !fir.ref<i64>) { | ||
%0 = fir.load %arg2 : !fir.ref<i64> | ||
%c0 = arith.constant 0 : index | ||
%1:3 = fir.box_dims %arg0, %c0 : (!fir.box<!fir.array<?x!fir.char<1,?>>>, index) -> (index, index, index) | ||
%2 = fir.array_load %arg0 : (!fir.box<!fir.array<?x!fir.char<1,?>>>) -> !fir.array<?x!fir.char<1,?>> | ||
%3 = fir.array_load %arg1 : (!fir.box<!fir.array<?x!fir.char<1,?>>>) -> !fir.array<?x!fir.char<1,?>> | ||
%c1 = arith.constant 1 : index | ||
%4 = arith.subi %1#1, %c1 : index | ||
%5 = fir.do_loop %arg3 = %c0 to %4 step %c1 unordered iter_args(%arg4 = %2) -> (!fir.array<?x!fir.char<1,?>>) { | ||
%6 = fir.array_access %3, %arg3 : (!fir.array<?x!fir.char<1,?>>, index) -> !fir.ref<!fir.char<1,?>> | ||
%7 = fir.array_access %arg4, %arg3 : (!fir.array<?x!fir.char<1,?>>, index) -> !fir.ref<!fir.char<1,?>> | ||
%false = arith.constant false | ||
%8 = fir.convert %7 : (!fir.ref<!fir.char<1,?>>) -> !fir.ref<i8> | ||
%9 = fir.convert %6 : (!fir.ref<!fir.char<1,?>>) -> !fir.ref<i8> | ||
fir.call @llvm.memmove.p0i8.p0i8.i64(%8, %9, %0, %false) : (!fir.ref<i8>, !fir.ref<i8>, i64, i1) -> () | ||
%10 = fir.array_amend %arg4, %7 : (!fir.array<?x!fir.char<1,?>>, !fir.ref<!fir.char<1,?>>) -> !fir.array<?x!fir.char<1,?>> | ||
fir.result %10 : !fir.array<?x!fir.char<1,?>> | ||
} | ||
fir.array_merge_store %2, %5 to %arg0 : !fir.array<?x!fir.char<1,?>>, !fir.array<?x!fir.char<1,?>>, !fir.box<!fir.array<?x!fir.char<1,?>>> | ||
return | ||
} | ||
func private @llvm.memmove.p0i8.p0i8.i64(!fir.ref<i8>, !fir.ref<i8>, i64, i1) | ||
} | ||
``` | ||
|
||
`fir.array_access` and `fir.array_amend` split the two purposes of | ||
`fir.array_update` into two distinct operations to work on type/kind that must | ||
reside in the memory reference domain. `fir.array_access` captures the array | ||
access semantics and `fir.array_amend` denotes which `fir.array_access` is the | ||
lhs. | ||
|
||
We do not want to start loading the entire `!fir.ref<!fir.char<1,?>>` here since | ||
it has dynamic length, and even if constant, could be too long to do so. | ||
|
||
## array_amend | ||
|
||
The `array_amend` operation marks an array value as having been changed via a | ||
reference obtain by an `array_access`. It acts as a logical transaction log | ||
that is used to merge the final result back with an `array_merge_store` | ||
operation. | ||
|
||
```mlir | ||
// fetch the value of one of the array value's elements | ||
%1 = fir.array_access %v, %i, %j : (!fir.array<?x?xT>, index, index) -> !fir.ref<T> | ||
// modify the element by storing data using %1 as a reference | ||
%2 = ... %1 ... | ||
// mark the array value | ||
%new_v = fir.array_amend %v, %2 : (!fir.array<?x?xT>, !fir.ref<T>) -> !fir.array<?x?xT> | ||
``` | ||
|
||
## Example | ||
|
||
Here is an example of a FIR code using several array operations together. The | ||
example below is a simplified version of the FIR code comiing from the | ||
following Fortran code snippet. | ||
|
||
```fortran | ||
subroutine s(a,l,u) | ||
type t | ||
integer m | ||
end type t | ||
type(t) :: a(:) | ||
integer :: l, u | ||
forall (i=l:u) | ||
a(i) = a(u-i+1) | ||
end forall | ||
end | ||
``` | ||
|
||
``` | ||
func @_QPs(%arg0: !fir.box<!fir.array<?x!fir.type<_QFsTt{m:i32}>>>, %arg1: !fir.ref<i32>, %arg2: !fir.ref<i32>) { | ||
%l = fir.load %arg1 : !fir.ref<i32> | ||
%l_index = fir.convert %l : (i32) -> index | ||
%u = fir.load %arg2 : !fir.ref<i32> | ||
%u_index = fir.convert %u : (i32) -> index | ||
%c1 = arith.constant 1 : index | ||
// This is the "copy-in" array used on the RHS of the expression. It will be indexed into and loaded at each iteration. | ||
%array_a_src = fir.array_load %arg0 : (!fir.box<!fir.array<?x!fir.type<_QFsTt{m:i32}>>>) -> !fir.array<?x!fir.type<_QFsTt{m:i32}>> | ||
// This is the "seed" for the "copy-out" array on the LHS. It'll flow from iteration to iteration and gets | ||
// updated at each iteration. | ||
%array_a_dest_init = fir.array_load %arg0 : (!fir.box<!fir.array<?x!fir.type<_QFsTt{m:i32}>>>) -> !fir.array<?x!fir.type<_QFsTt{m:i32}>> | ||
%array_a_final = fir.do_loop %i = %l_index to %u_index step %c1 unordered iter_args(%array_a_dest = %array_a_dest_init) -> (!fir.array<?x!fir.type<_QFsTt{m:i32}>>) { | ||
// Compute indexing for the RHS and array the element. | ||
%u_minus_i = arith.subi %u_index, %i : index // u-i | ||
%u_minus_i_plus_one = arith.addi %u_minus_i, %c1: index // u-i+1 | ||
%a_src_ref = fir.array_access %array_a_src, %u_minus_i_plus_one {Fortran.offsets} : (!fir.array<?x!fir.type<_QFsTt{m:i32}>>, index) -> !fir.ref<!fir.type<_QFsTt{m:i32}>> | ||
%a_src_elt = fir.load %a_src_ref : !fir.ref<!fir.type<_QFsTt{m:i32}>> | ||
// Get the reference to the element in the array on the LHS | ||
%a_dst_ref = fir.array_access %array_a_dest, %i {Fortran.offsets} : (!fir.array<?x!fir.type<_QFsTt{m:i32}>>, index) -> !fir.ref<!fir.type<_QFsTt{m:i32}>> | ||
// Store the value, and update the array | ||
fir.store %a_src_elt to %a_dst_ref : !fir.ref<!fir.type<_QFsTt{m:i32}>> | ||
%updated_array_a = fir.array_amend %array_a_dest, %a_dst_ref : (!fir.array<?x!fir.type<_QFsTt{m:i32}>>, !fir.ref<!fir.type<_QFsTt{m:i32}>>) -> !fir.array<?x!fir.type<_QFsTt{m:i32}>> | ||
// Forward the current updated array to the next iteration. | ||
fir.result %updated_array_a : !fir.array<?x!fir.type<_QFsTt{m:i32}>> | ||
} | ||
// Store back the result by merging the initial value loaded before the loop | ||
// with the final one produced by the loop. | ||
fir.array_merge_store %array_a_dest_init, %array_a_final to %arg0 : !fir.array<?x!fir.type<_QFsTt{m:i32}>>, !fir.array<?x!fir.type<_QFsTt{m:i32}>>, !fir.box<!fir.array<?x!fir.type<_QFsTt{m:i32}>>> | ||
return | ||
} | ||
``` |