Compilation of array expressions in Fortran
As I stated in my previous post, Fortran 90 improved the array capabilities of Fortran. Today we will discuss what are the challenges when compiling array expressions.
A new feature of Fortran 90 is the possibility of expressing array-wise calculations. So it is possible to do this.
REAL :: A(N), B(N), C(N) A = 1.0 B = 2.0 C = A + B
The first two assignments are correct because Fortran 90 allows promoting (conforming in the Standard parlance) a scalar to an array of some rank. So they set all the N elements of A and B to 1 and 2 respectively.
The third assignment is maybe more interesting.
C is assigned the sum of the N elements of A and B. So each
C(I) will have the value of
A(I) + B(I).
Not only intrinsic operations can be used in the context of an array expressions. A procedure (i.e. a function or subroutine) that is
ELEMENTAL can be called with array arguments, all of the same rank or promoteable to it. If the procedure is a function, it will return an array as well. Many intrinsics are defined as ELEMENTAL and the user can defined new ELEMENTAL functions. There are a few reasonable restrictions for ELEMENTAL functions: their arguments and return must be scalars and should not have side effects, so they can be coherently extended to any rank and the order of application of the function should not be relevant. Used judiciously, this can end in very compact and expressive code.
INTEGER :: A(N), B(N), SAD IF (ANY(A /= B)) THEN PRINT *, "Some value in A is different to the corresponding in B" END IF ! Sum of absolute differences SAD = SUM(ABS(A - B))
In the examples above the arrays have rank 1, but nothing prevents to use this for arrays of higher rank. Note though, that a lower-ranked array is not conformed to a higher-ranked array, only scalars can be conformed this way. This means that all arrays involved in the array expression will always have the same rank. Not only this, they must have the same
SIZE for each rank. This means that the number of elements of each rank should match.
REAL :: M1(N, M), M2(N, M), M3(N, M) ! Addition of two matrices M3 = M1 + M2
Also array sections can be used liberally in the expression as long as the sizes of the ranks match.
INTEGER :: A(N), B(N), C(N) A = A(N:1:-1) ! Reverses A C(1:2) = A(3:4) + B(5:4:-1) ! Does ! C(1) = A(3) + B(5) ! C(2) = A(4) + B(4) C(1:4:2) = A(3:10:4) + B(5:4-1) ! Does ! C(1) = A(3) + B(5) ! C(3) = A(7) + B(4)
Finally all this can be applied to pointers as well.
Compiling array expressions
Seems obvious at first that an array expression like
A = B + C
should be ideally compiled as something like
DO I = 1, N A(I) = B(I) + C(I) END DO
And functionally we want something like that, except that it does not always work. If you check the example above where we reversed an array doing
A = A(N:1:-1) because doing
DO I = 1, N A(I) = A(N - I + 1) END DO
is simply wrong.
A way that will always work will be doing first the right hand side of the assignment and keeping it into a temporary, and then move from the temporary to the left hand side. Retaking
A = B + C a way could be
DO I = 1, N TMP(I) = B(I) + C(I) END DO DO I = 1, N A(I) = TMP(I) END DO
Which looks wasteful. First because given that the sizes of the rank will match, the two loops will do the same number of iterations for each operand and operation. So we might want to compile this like:
DO I = 1, N TMP(I) = B(I) + C(I) A(I) = TMP(I) END DO
This, though, still looks off, basically because in this case we do not need
TMP at all.
DO I = 1, N A(I) = B(I) + C(I) END DO
This is exactly our intuitive first approach.
Unfortunately this is not always correct. In the reversal of the array, though, we cannot fuse the two loops.
DO I = 1, N TMP(I) = A(N - I + 1) END DO DO I = 1, N A(I) = TMP(I) END DO
Note also that this way of reversing an array is rather uninspired as it would be enough iterating up to
N/2 and then just doing
A(I) = A(N - I + 1).
We have seen that we may need to introduce a temporary in order to evaluate the right hand side of an array assignment. The question is, can we avoid this and directly write to the result? We have seen that there are cases where this is feasible, can we tell which ones?
TMP means that we will update each element of the left hand side. This is OK as long nothing in the right hand side must evaluate to any of the values written in the right hand side. That would create a dependence and we cannot violate them.
Dependences between array accesses depend on two elements: first the array object subscripted. If the two array objects are clearly different there will never be any dependence.
INTEGER :: A(10), B(10), C(10) C = A + B ! No dependencies as A, B and C ! are different arrays.
If the two objects are the same, or can potentially be the same (more on this below), then there may be a potential dependency. A conservative and fast (in compilation time) is assume that in this case the dependence exists. This may be overly conservative but makes for a simple implementation.
INTEGER :: A(10), B(10) A = 1 A(1:5) = A(1:5) + A(2:6) ! (*) B = 1 B(2:6) = B(2:6) + B(1:5) ! (**)
While this is simple approach is safe there is still some room for improvement. If you check the assignment in
(*), we are in practice doing
A(I) = A(I) + A(I+1), with
I = 1..5. This means that it there is no dependence between
A(I+1). Compare this case with the assignment in
(**). Here in practice we are doing
B(I) = B(I) + B(I-1) with
I = 2..6. This second case would be reading a value already updated in an earlier iteration. But, note that if instead of
I = 2..6 we do
I = 6..2 this would work!
So if the two objects may potentially be the same, we still may want to analyze the indexes because the regions of the array are updated in an order that does not violate dependences. This kind of dependency based optimizations have been extensively studied in the textbook by Allen and Kennedy. We will not discuss them here, just let’s get the idea that these loop optimizations are useful in this case.
Before we attempt to analyze the indexes we may want to tell if two objects may refer to the same data in memory. When two names can refer the same data we say that they alias. Fortran makes relatively easy to tell when two objects may or may not alias. We use “may” here because in some times we need to assume they may alias even if they do not in practice. We may not have enough information at this point.
Given two names, they obviously may alias if they have the same name. If they have different names and neither is a
POINTER then they do not alias unless both appear in the same
EQUIVALENCE set. If only one is
POINTER and the other cannot be a
TARGET of the pointer, then they do not alias. Otherwise we have to assume they may alias. This includes cases where we operate two
POINTERs or a
POINTER and a potential
TARGET of the first.
An important feature of Fortran is that parameters that are not
POINTER never alias with other parameters. This is also valid for arrays.
More complicated expressions
An array assignment can be more complicated but only in the right hand side. The left hand side is more or less regular in practical terms. For instance consider
INTEGER :: A(10), B(10), C(10), D(10) D = A * B + C
Note that now we have two subexpressions, the one with T ← A * B and the one with T + C. But this is where the complexity ends. Due to the nature of the right hand side, which cannot have side-effects to the existing variables, it is safe to reuse T all the time. Like this.
INTEGER :: A(10), B(10), C(10), D(10) INTEGER :: TMP(10) DO I = 1, 10 TMP(I) = A(I) * B(I) TMP(I) = TMP(I) + C(I) D(I) = TMP(I) END DO
Note that we may be tempted to scalarize the
TMP(I) variable and this would be possible in the case above.
INTEGER :: A(10), B(10), C(10), D(10) INTEGER :: TMP DO I = 1, 10 TMP = A(I) * B(I) TMP = TMP + C(I) D(I) = TMP END DO
Again, though, we already know this is not something that can be done in general.
INTEGER :: A(10), B(10), C(10), D(10) D = A * B + C + D(10:1:-1)
Here we are forced to evaluate the whole
INTEGER :: A(10), B(10), C(10), D(10) INTEGER :: TMP(10), TMP2 DO I = 1, 10 TMP(I) = D(11 - I) END DO DO I = 1, 10 TMP2 = A(I) * B(I) TMP2 = TMP2 + C(I) TMP2 = TMP2 + TMP(I) D(I) = TMP2 END DO
Non-assignment array expressions
If the array expression does not involve any assignment, some of the complexity is simplified. For instance in the expression above
ANY(A /= B) this can be modeled as an assignment to a temporary. That temporary does not alias with any other variable, so this can be implemented with a single loop, even in the presence of complex array sections.
LOGICAL :: TMP(N), TMP2 DO I = 1, N TMP(I) = A(I) /= B(I) END DO TMP2 = ANY(TMP) IF (TMP2) THEN ...
Who says that Fortran is not an interesting language for compilers? 🙂