| <!--===- 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 |
| |
| ```{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. |
| |
| ``` |
| %s = fir.shape_shift %lb1, %ex1, %lb2, %ex2 : (index, index, index, index) -> !fir.shapeshift<2> |
| // load the entire array 'a' |
| %v = fir.array_load %a(%s) : (!fir.ref<!fir.array<?x?xf32>>, !fir.shapeshift<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. |
| |
| ``` |
| %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 chasing 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. |
| |
| ``` |
| %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. |
| |
| ``` |
| %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. |
| |
| ``` |
| %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. |
| |
| ``` |
| // 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 |
| } |
| ``` |