4.6 Initial evaluation
6.1.2 Non-de-sugarable differences
More significant distinctions between SaC and SaC-λ are concerned with shape- invariant array programming and more advanced map/reduce syntax.
Shape-invariant programming One of the strong features of SaC is its ability to handle arrays and operations on them without explicit knowledge of their shapes or even ranks. This is achieved by introducing special types, which allow one to define arrays where only the rank or the base type is known.
int[.,.,.] Three-dimensional array with element- type int; exact size at each axis will be known at runtime only.
float[*] An array with element-type float of un- known rank and size. Can be also scalar. The least precise type in SaC.
To handle data of such types SaC introduces two built-in operators, namely shape and dim:
shape Returns a vector containing sizes of
an array at every axis. For exam- ple, shape ([[1,2],[3,4]]) will return [2,2].
dim Returns the rank of an array. For example,
dim ([[1,2],[3,4]]) will return 2.
Note that when using those operators we introduce a dependency between type components and values of a program. Consider the following example:
i n t foo ( int [ . , . , . ] x ) {
return sum ( shape ( x ) ) ; }
Here the components of the array type become values. Now consider: i n t[ ∗ ] bar ( int x )
{
return genarray ( [ x , x ] , 0 ) ; }
In this case, a value becomes a part of the type of a generated array.
This has an impact on the layout transformations we can make. First of all, our system cannot deal with arrays of unknown dimension. That is because Map[△] and Idx[△] refer to the length of an index vector. In case all the array ranks are known at compile time, we can compute the length of any index vector as well. In case the length of an index vector is not known, our type constraints cannot be resolved immediately, which makes our algorithm inapplicable in the form it is presented in this thesis. Although it might be possible to postpone constraint resolutions, this is out of the scope of this thesis.
Secondly, when using shape on the transformed programs with new data layouts it has to return the original values, otherwise transformed programs will evaluate wrong results — consider function foo above as an example. This means that we either have to keep the original shapes, or recompute them from the new shapes. Unfortunately, the latter is not possible due to the paddings we use. If the original shape is [N, N], for the layout type 2, the new shape will be [N, (N + V − 1) div V, V ], and integer division is irreversible. We solve this problem by introducing a new component of the array in SaC programs — the original shape. Originally every SaC value was formally described with a pair ⟨shape, data⟩. After the transformation we extend this tuple with a new component, the original shape: ⟨shape, orig_shape, data⟩, and we make sure that all applications of shape return the original shape.
Extended map/reduce Map and reduce operators in SaC-λ are restricted ver- sions of the with-loop construct in SaC. The main difference is that map and reduce in SaC-λ do not allow one to update regions of an iteration space. That is:
map i < u f ( i )
generates an index space of shape u and returns the result which is also of the shape u. That implies that in order to update a region of an array, the body of a map has to contain a condition. For example, in the case when we want to fill a region from [20,20] to [40,40] with evaluated expression and make all the other elements of the array a of shape [100,100] to be zero, we will have to write the following SaC-λ code:
a = map i < [ 1 0 0 , 1 0 0 ]
i f i [ 0 ] >= 20 and i [ 0 ] < 40
and i [ 1 ] >= 20 and i [ 1 ] < 40 then f ( i )
e l s e 0
Semantically equivalent SaC code will look like this: a = with {
( [ 2 0 , 2 0 ] <= i < [ 4 0 , 4 0 ] ) : f ( i ) ; } : genarray ( [ 1 0 0 , 1 0 0 ] , 0)
Both codes evaluate to the same result. However, during the execution, a com- parison on every iteration results in a large performance penalty. To avoid this SaC generates code that splits the overall iteration space in subspaces, where all elements in a subspace are evaluated unconditionally. In the given example we can evaluate
zero on subspaces: ([0,0] to [20,100]), ([40,0] to [100,100]), ([20,0] to [40,20]) and ([20,40] to [40,100]); and we evaluate f (i) on the subspace ([20,20] to [40,40]). In order to perform such an optimisation the subspaces have to be identified and as the SaC form is more restrictive than the SaC-λ form with a general condition, S a C is capable to perform more aggressive optimisations. This is a property that we would like to preserve, which means that our transformations have to deal with region-based map/reduces rather than with whole-range map/reduces.
Another form of with-loops in SaC that avoids a condition in the body is modarray with-loops. It is a very common pattern that a region of an array e.g. a single element has to be updated and all the other elements should be copied. Consider an example when we update an element of a two-dimensional array a of shape [100,100] at position [1,1] with a value x. The relevant SaC-λ code will look as follows: a ’ = map i < [ 1 0 0 , 1 0 0 ] i f i [ 0 ] == 1 and i [ 1 ] == 1 then x e l s e a [ i ]
Semantically equivalent SaC code will look like: a ’ = with {
( [ 1 , 1 ] <= i < [ 2 , 2 ] ) : x ; } : modarray ( a )
Again, the reason for having a separate construction for such a case is to make sure that the generated code will not execute condition on every iteration.