ArraysΒΆ
This primer is a tutorial on Chapel rectangular arrays and domains.
Arrays in Chapel are specified using a square-bracketed expression that specifies the array's index set, followed by the array's element type. Rectangular arrays are those whose indices are integers or tuples of integers, supporting standard multidimensional, rectilinear index sets.
config const n = 5;
Declare an array of five 64-bit real values:
var A: [1..n] real;
Like other variable types in Chapel, arrays are initialized so that each element stores its default value. So our array of real values above will default to an array of the value 0.0.
writeln("Initially, A is: ", A);
Arrays can also be declared using the the array literal syntax. Array literals are specified by enclosing a comma separated list of expressions in square brackets. The domain of the array will be 1-based in each dimension, and the type of the array's element is the type of the first element listed.
var A2 = [-1.1, -2.2, -3.3, -4.4, -5.5];
writeln("Initially, A2 is: ", A2);
Arrays can be accessed using scalar index values of the appropriate type, using either parentheses or square brackets:
A[1] = 1.1;
A(2) = 2.2;
writeln("After assigning two elements, A is: ", A);
Arrays can also be accessed using ranges to refer to subsets of array elements, or sub-arrays, using a technique called slicing:
A[2..4] = 3.3;
writeln("After assigning its interior values, A is: ", A);
writeln();
Just like with array indexing, either square brackets or parentheses can be used for array slicing:
writeln("A(2..4) is: ", A(2..4), "\n");
Note: further information on slicing can be found in the Slices Primer
Arrays can be multidimensional as well:
var B: [1..n, 1..n] real;
forall (i,j) in {1..n, 1..n} do
B(i,j) = i + j/10.0;
writeln("Initially, B is:\n", B, "\n");
An array's elements can be iterated over in a for or forall loop, which causes the index variable to store references to an array's elements:
forall b in B do
b += 1;
writeln("After incrementing B's elements, B is:\n", B, "\n");
An array's index set is referred to as a domain -- a first-class
language concept that stores the set of indices used to access the
array. The arrays above are declared with the anonymous domains
{1..n}
and {1..n, 1..n}
. An array's domain can be accessed
using the .domain
method:
forall (i,j) in B.domain do
B(i,j) -= 1;
writeln("After decrementing B's elements, B is:\n", B, "\n");
Domains can also be queried when arrays are passed to functions in order to refer to the domain via a new identifier within the context of that function:
printArr(B);
proc printArr(X: [?D] real) {
writeln("within printArr, D is: ", D, "\n");
forall (i,j) in D do
X(i,j) = -X(i,j);
writeln("after negating X within printArr, X is:\n", X, "\n");
}
Arrays are passed to functions by reference by default, so the modifications to X in function printArr are reflected on B as well:
writeln("After calling printArr, B is:\n", B, "\n");
Domains can also be declared and named. This has the advantages of allowing them to be reused for multiple arrays, for associating names with different index spaces, and for amortizing the overhead of storing distributed arrays across multiple similar array variables.
The following domain declaration defines a 2D arithmetic domain
called ProbSpace
which is the same size and shape as B
was above.
var ProbSpace: domain(2) = {1..n, 1..n};
We then use that domain to declare some arrays...
var C, D, E: [ProbSpace] bool;
...and to iterate over their index spaces...
for (i,j) in ProbSpace do
C(i,j) = (i+j) % 3 == 0;
writeln("After initializing C, its value is:\n", C, "\n");
When indexing over a multidimensional domain, the indices
can be captured into a single tuple variable rather than
destructuring the tuple into its integer components.
Similarly, multidimensional array accesses can be expressed using
tuple indices rather than multiple integer arguments. In the
following example, the index variable ij stores a 2-tuple of
integers (2*int
in Chapel). This is a really inefficient way
to assign the diagonal values "true" -- note the use of tuple
indexing to tease the individual components out of the 2-tuple.
for ij in ProbSpace do
D(ij) = ij(1) == ij(2);
writeln("After initializing D, its value is:\n", D, "\n");
Arrays of similar size and shape support whole-array assignment.
E = C;
writeln("After assigning C to E, E's value is:\n", E, "\n");
Whole array assignment also allows scalar values to be promoted and assigned to every element of an array.
B = 0.0;
writeln("After being reset, B is:\n", B, "\n");
An array need not be indexed using the domain used to declare it,
though doing so presents the compiler with opportunities to
optimize bounds checks away. In the following loop, there is
no known relation between B
and ProbSpace
, so bounds checks are
harder to prove away (requires symbolic analysis of the definitions
of the two domains and the invariance of their bounds).
for (i,j) in ProbSpace do
B(i,j) = i + j/10.0;
writeln("B has been re-initialized to:\n", B, "\n");
Whole-array assignment can also be used for arrays or sub-arrays whose index spaces differ. Their shapes must still match.
var F, G: [ProbSpace] real;
F[2..n-1, 2..n-1] = B[1..n-2, 3..n];
writeln("After assigning a slice of B to F, F's value is:\n", F, "\n");
Arrays can also be sliced using unbounded ranges in which either the low and/or high bounds are omitted. In this case, they will be inherited from the array's bounds.
G[2.., ..] = B[..n-1, ..];
writeln("After assigning a slice of B to G, G's value is:\n", G, "\n");
Array slicing supports rank-change semantics when sliced using
a scalar value rather than a range. In the following assignment,
recall that A
was our initial 1-dimensional array.
A = B[n/2, ..];
writeln("After being assigned a slice of B, A is:\n", A, "\n");
Domains can also be sliced. However, rather than having indexing semantics, domain slicing results in intersection of the domain's index set and the specified slice.
Domain slicing, like array indexing and slicing, can be written with either square brackets or parentheses.
writeln("ProbSpace[1..n-2, 3..] is: ", ProbSpace[1..n-2, 3..], "\n");
Ranges also support slicing in this way, though we don't demonstrate that here.
Domain variables and expressions can also be used to specify an array slice rather than using lists of ranges. For example:
const ProbSpaceSlice = ProbSpace[0..n+1, 3..];
writeln("B[ProbSpaceSlice] is:\n", B[ProbSpaceSlice], "\n");
Forall loops over domains and arrays can be written using the
syntax [<ind> in <Dom>] ...
which is shorthand for
forall <ind> in <Dom> do ...
const offset = (1,1); // a 2-tuple offset
[ij in ProbSpace[2..n-1, 2..n-1]] F(ij) = B(ij + offset);
writeln("After assigning F a shifted slice of B, it is:\n", F, "\n");
[b in B] b = -b;
writeln("After negating B, it is:\n", B, "\n");
Note that this shorthand resembles the array type definition in a variable declaration.
Another advantage to declaring named domain variables is that their index sets can be reassigned. This results in a logical re-allocation of the array variable in question, preserving array values for indices that existed in both the old and new domain values:
var VarDom = {1..n};
var VarArr: [VarDom] real = [i in VarDom] i;
writeln("Initially, VarArr = ", VarArr, "\n");
Now, if we reassign VarDom
, VarArr
will be reallocated with the
old values preserved and the new values initialized to the element
type's default value.
VarDom = {1..2*n};
writeln("After doubling VarDom, VarArr = ", VarArr, "\n");
As mentioned before, this reallocation preserves values according
to index, so if we extend the lower bound of the domain, the
non-zero values will still logically be associated with indices
1..n
:
VarDom = {-n+1..2*n};
writeln("After lowering VarDom's lower bound, VarArr = ", VarArr, "\n");
If the domain shrinks, values will be thrown away
VarDom = {2..n-1};
writeln("After shrinking VarDom, VarArr = ", VarArr, "\n");
One trick to reallocate an array without preserving any values is
to assign its domain variable a degenerate domain, e.g. {1..0}
,
and then assign it the new value:
VarDom = {1..0};
writeln("VarArr should now be empty: ", VarArr, "\n");
VarDom = {1..n};
writeln("VarArr should now be reset: ", VarArr, "\n");
Note that querying an array's domain via the .domain
method or
the function argument query syntax does not result in a domain
expression that can be reassigned. In particular, we cannot do:
VarArr.domain = {1..2*n};
nor:
proc foo(X: [?D]) { D = {1..2*n}; }
Only a domain variable or formal argument can be
reassigned to reallocate arrays. This is to avoid confusion
since assigning one domain variable can cause a number of
arrays to be reallocated. It also implies that arrays declared
using an anonymous domain cannot be reallocated. So for our
original array declarations A
and B
, we have no way of reallocating
them. Arrays with constant domains provide the compiler with
optimization benefits, so this supports a common case efficiently.
As some of our examples have shown, arrays in Chapel can have arbitrary element types -- numeric values, classes, or records. Arrays can also support array elements, and initial support for this is implemented in our compiler. For example:
var Y: [ProbSpace] [1..3] real;
forall (i,j) in ProbSpace do
for k in 1..3 do
Y(i,j)(k) = i*10 + j + k/10.0;
writeln("Y is:\n", Y);
Our current implementation requires that array elements must all be of uniform size. We would also like to support jagged arrays, where the inner array size is a function of the outer. In particular, it is our intention to support arrays like these:
var Triangle: [row in 1..n] [1..row] real;
var HierArr: [lvl in 1..n] [1..2**lvl, 1..2**lvl] real;
For further information, see the Domain Primer and other array primers: Sparse, Opaque, Associative.