Indexing

Indexing in ArrayFire is a powerful but easy to abuse feature of the af::array class. This feature allows you to reference or copy subsections of a larger array and perform operations on only a subset of elements.

Indexing in ArrayFire can be performed using the parenthesis operator or one of the member functions of the af::array class. These functions allow you to reference one or a range of elements from the original array.

Here we will demonstrate some of the ways you can use indexing in ArrayFire and discuss ways to minimize the memory and performance impact of these operations.

Lets start by creating a new 4x4 matrix of floating point numbers:

float data[] = {0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
12, 13, 14, 15};
af::array A(4, 4, data);

ArrayFire is column-major so the resulting A array will look like this:

$\begin{bmatrix} 0 & 4 & 8 & 12 \\ 1 & 5 & 9 & 13 \\ 2 & 6 & 10 & 14 \\ 3 & 7 & 11 & 15 \end{bmatrix}$

This is a two dimensional array so we can access the first element of this matrix by passing 0,0 into the parenthesis operator of the af::array.

// Returns an array pointing to the first element
A(2, 3); // WARN: avoid doing this. Demo only

$A(2, 3) = [ 14 ]$

We can also access the array using linear indexing by passing in one value. Here we are accessing the fifth element of the array.

// Returns an array pointing to the fifth element
A(5);

$A(5) = [ 5 ]$

Note
Normally you want to avoid accessing individual elements of the array like this for performance reasons.

Indexing with negative values will access from the end of the array. For example, the value negative one and negative two(-2) will return the last and second to last element of the array, respectively. ArrayFire provides the end alias for this which also allows you to index the last element of the array.

array ref0 = A(2, -1); // 14 second row last column
array ref1 = A(2, end); // 14 Same as above
array ref2 = A(2, -2); // 10 Second row, second to last(third) column
array ref3 = A(2, end-1); // 10 Same as above

# Indexing slices and subarrays

You can access regions of the array via the af::seq and af::span objects. The span objects allows you to select the entire set of elements across a particular dimension/axis of an array. For example, we can select the third column of the array by passing span as the first argument and 2 as the second argument to the parenthesis operator.

// Returns an array pointing to the third column
A(span, 2);

$A(span, 2) = \begin{bmatrix} 8 \\ 9 \\ 10 \\ 11 \end{bmatrix}$

You can read that as saying that you want all values across the first dimension, but only from index 2 of the second dimension.

You can access the second row by passing (1, span) to the array

// Returns an array pointing to the second row
A(1, span);

$A(1, span) = [ 1, 5, 9, 13 ]$

You can use the af::seq (short for sequence) object to define a range when indexing. For example, if you want to get the first two columns, you can access the array by passing af::span for the first argument and af::seq(2) as the second argument.

// Returns an array pointing to the first two columns
A(span, seq(2));

$A(span, seq(2)) = \begin{bmatrix} 0 & 4 \\ 1 & 5 \\ 2 & 6 \\ 3 & 7 \end{bmatrix}$

There are three constructors for af::seq.

The last constructor that can help create non-continuous ranges. For example, you can select the second and forth(last) rows by passing (seq(1, end, 2), span) to the indexing operator.

// Returns an array pointing to the second and fourth rows
A(seq(1, end, 2), span);

$A(seq(1, end, 2), span) = \begin{bmatrix} 1 & 5 & 9 & 13 \\ 3 & 7 & 11 & 15 \end{bmatrix}$

# Indexing using af::array

You can also index using other af::array objects. ArrayFire performs a Cartesian product of the input arrays.

vector<int> hidx = {2, 1, 3};
vector<int> hidy = {3, 1, 2};
array idx(3, hidx.data());
array idy(3, hidy.data());
array out = A(idx, idy);

$A = \begin{bmatrix} 0 & 4 & 8 & 12 \\ 1 & 5 & 9 & 13 \\ 2 & 6 & 10 & 14 \\ 3 & 7 & 11 & 15 \end{bmatrix} \\ A( \begin{bmatrix} 2 \\ 1 \\ 3 \end{bmatrix} , \begin{bmatrix} 3 \\ 1 \\ 2 \end{bmatrix} ) = \begin{bmatrix} (2,3) & (2,1) & (2,2) \\ (1,3) & (1,1) & (1,2) \\ (3,3) & (3,1) & (3,2) \end{bmatrix} = \begin{bmatrix} 14 & 6 & 10 \\ 13 & 5 & 9 \\ 15 & 7 & 11 \end{bmatrix}$

If you want to index an af::array using coordinate arrays, you can do that using the af::approx1 and af::approx2 functions.

vector<float> hidx = {2, 1, 3};
vector<float> hidy = {3, 1, 2};
array idx(3, hidx.data());
array idy(3, hidy.data());
array out = approx2(A, idx, idy);

$approx2(A, \begin{bmatrix} 2 \\ 1 \\ 3 \end{bmatrix} , \begin{bmatrix} 3 \\ 1 \\ 2 \end{bmatrix} ) = \begin{bmatrix} (2,3) \\ (1,1) \\ (3,2) \end{bmatrix} = \begin{bmatrix} 14 \\ 5 \\ 11 \end{bmatrix}$

Boolean(b8) arrays can be used to index into another array. In this type of indexing the non-zero values will be selected by the boolean operation. If we want to select all values less than 5, we can pass a boolean expression into the parenthesis operator.

array out = A(A < 5);

$out = \begin{bmatrix} 0 \\ 1 \\ 2 \\ 3 \\ 4 \end{bmatrix}$

# References and copies

All ArrayFire indexing functions return af::array(technically its an array_proxy class) objects. These objects may be new arrays or they may reference the original array depending on the type of indexing that was performed on them.

• If an array was indexed using another af::array or it was indexed using the af::approx functions, then a new array is created. It does not reference the original data.
• If an array was indexed using a scalar, af::seq or af::span, then the resulting array will reference the original data IF the first dimension is continuous. The following lines will not allocate additional memory.
Note
The new arrays wither references or newly allocated arrays, are independent of the original data. Meaning that any changes to the original array will not propagate to the references. Likewise, any changes to the reference arrays will not modify the original data.
array reference = A(span, 1);
array reference2 = A(seq(3), 1);
array reference3 = A(seq(2), span);

The following code snippet shows some examples of indexing that will allocate new memory.

array copy = A(2, span);
array copy2 = A(seq(1, 3, 2), span);
int hidx[] = {0, 1, 2};
array idx(3, hidx);
array copy3 = A(idx, span);

Notice that even though the copy3 array is referencing continuous memory in the original array, a new array is created because we used an array to index into the af::array.

# Assignment

An assignment on an af::array will replace the array with the result of the expression on the right hand side of the equal(=) operator. This means that the type and shape of the result can be different from the array on the left had side of the equal operator. Assignments will not update the array that was previously referenced through an indexing operation. Here is an example:

array inputA = constant(3, 10, 10);
array inputB = constant(2, 10, 10);
array data = constant(1, 10, 10);
// Points to the second column of data. Does not allocate memory
array ref = data(span, 1);
// This call does NOT update data. Memory allocated in matmul
ref = matmul(inputA, inputB);
// reference does not point to the same memory as the data array

The ref array is created by indexing into the data array. The initialized ref array points to the data array and does not allocate memory when it is created. After the matmul call, the ref array will not be pointing to the data array. The matmul call will not update the values of the data array.

You can update the contents of an af::array by assigning with the operator parenthesis. For example, if you wanted to change the third column of the A array you can do that by assigning to A(span, 2).

array reference = A(span, 2);
A(span, 2) = 3.14f;
assert(allTrue<bool>(reference != A(span, 2)));

$ref = \begin{bmatrix} 8 \\ 9 \\ 10 \\ 11 \end{bmatrix} A = \begin{bmatrix} 0 & 4 & 3.14 & 12 \\ 1 & 5 & 3.14 & 13 \\ 2 & 6 & 3.14 & 14 \\ 3 & 7 & 3.14 & 15 \end{bmatrix}$

This will update only the array being modified. If there are arrays that are referring to this array because of an indexing operation, those values will remain unchanged.

Allocation will only be performed if there are other arrays referencing the data at the point of assignment. In the previous example, an allocation will be performed when assigning to the A array because the ref array is pointing to the original data. Here is another example demonstrating when an allocation will occur:

{
// No allocation performed. ref points to A's memory
array ref = A(span, 2);
} // ref goes out of scope. No one point's to A's memory
A(span, 2) = 3.14f; // No allocation performed.

In this example, no allocation will take place because when the ref object is created, it is pointing to A's data. Once it goes out of scope, no data points to A, therefore when the assignment takes place, the data is modified in place instead of being copied to a new address.

You can also assign to arrays using another af::arrays as an indexing array. This works in a similar way to the other types of assignment but care must be taken to assure that the indexes are unique. Non-unique indexes will result in a race condition which will cause non-deterministic values.

vector<int> hidx = {4, 3, 4, 0};
vector<float> hvals = {9.f, 8.f, 7.f, 6.f};
array idx(4, hidx.data());
array vals(4, hvals.data());
A(idx) = vals; // nondeterministic. A(4) can be 9 or 7

$idx = \begin{bmatrix} 4 \\ 3 \\ 4 \\ 0 \end{bmatrix} vals = \begin{bmatrix} 9 \\ 8 \\ 7 \\ 6 \end{bmatrix} \\ A = \begin{bmatrix} 6 & 9\ or\ 7 & 8 & 12 \\ 1 & 5 & 9 & 13 \\ 2 & 6 & 10 & 14 \\ 8 & 7 & 11 & 15 \end{bmatrix}$

# Member functions

There are several member functions which allow you to index into an af::array. These functions have similar functionality but may be easier to parse for some.

See Assignment & Indexing operation on arrays for the full listing.

array A = array(seq(1, 9), 3, 3);
// 1.0000 4.0000 7.0000
// 2.0000 5.0000 8.0000
// 3.0000 6.0000 9.0000
af_print(A(0)); // first element
// 1.0000
af_print(A(0, 1)); // first row, second column
// 4.0000
af_print(A(end)); // last element
// 9.0000
af_print(A(-1)); // also last element
// 9.0000
af_print(A(end - 1)); // second-to-last element
// 8.0000
af_print(A(1, span)); // second row
// 2.0000 5.0000 8.0000
af_print(A.row(end)); // last row
// 3.0000 6.0000 9.0000
af_print(A.cols(1, end)); // all but first column
// 4.0000 7.0000
// 5.0000 8.0000
// 6.0000 9.0000
float b_host[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array b(10, 1, b_host);
af_print(b(seq(3)));
// 0.0000
// 1.0000
// 2.0000
af_print(b(seq(1, 7)));
// 1.0000
// 2.0000
// 3.0000
// 4.0000
// 5.0000
// 6.0000
// 7.0000
af_print(b(seq(1, 7, 2)));
// 1.0000
// 3.0000
// 5.0000
// 7.0000
af_print(b(seq(0, end, 2)));
// 0.0000
// 2.0000
// 4.0000
// 6.0000
// 8.0000

You can set values in an array:

array A = constant(0, 3, 3);
// 0.0000 0.0000 0.0000
// 0.0000 0.0000 0.0000
// 0.0000 0.0000 0.0000
// setting entries to a constant
A(span) = 4; // fill entire array
// 4.0000 4.0000 4.0000
// 4.0000 4.0000 4.0000
// 4.0000 4.0000 4.0000
A.row(0) = -1; // first row
// -1.0000 -1.0000 -1.0000
// 4.0000 4.0000 4.0000
// 4.0000 4.0000 4.0000
A(seq(3)) = 3.1415; // first three elements
// 3.1415 -1.0000 -1.0000
// 3.1415 4.0000 4.0000
// 3.1415 4.0000 4.0000
// copy in another matrix
array B = constant(1, 4, 4, s32);
// 1 1 1 1
// 1 1 1 1
// 1 1 1 1
// 1 1 1 1
B.row(0) = randu(1, 4, f32); // set a row to random values (also upcast)
// The first rows are zeros because randu returns values from 0.0 - 1.0
// and they were converted to the type of B which is s32
// 0 0 0 0
// 1 1 1 1
// 1 1 1 1
// 1 1 1 1

Use one array to reference into another.

float h_inds[] = {0, 4, 2, 1}; // zero-based indexing
array inds(1, 4, h_inds);
af_print(inds);
// 0.0000 4.0000 2.0000 1.0000
array B = randu(1, 4);
// 0.5471 0.3114 0.5535 0.3800
array c = B(inds); // get
// 0.5471 0.3800 0.5535 0.3114
B(inds) = -1; // set to scalar
B(inds) = constant(0, 4); // zero indices
// 0.0000 0.0000 0.0000 0.0000
af::matmul
AFAPI array matmul(const array &lhs, const array &rhs, const matProp optLhs=AF_MAT_NONE, const matProp optRhs=AF_MAT_NONE)
Matrix multiply of two arrays.
af::constant
array constant(T val, const dim4 &dims, const dtype ty=(af_dtype) dtype_traits< T >::ctype)
af::array
A multi dimensional data container.
Definition: array.h:35
af_print
#define af_print(...)
Definition: util.h:148
af::approx2
AFAPI array approx2(const array &in, const array &pos0, const array &pos1, const interpType method=AF_INTERP_LINEAR, const float off_grid=0.0f)
C++ Interface for data interpolation on two-dimensional signals.
afcl::array
static af::array array(af::dim4 idims, cl_mem buf, af::dtype type, bool retain=false)
Create an af::array object from an OpenCL cl_mem buffer.
Definition: opencl.h:327
af::randu
AFAPI array randu(const dim4 &dims, const dtype ty, randomEngine &r)
s32
@ s32
32-bit signed integral values
Definition: defines.h:216
af::span
AFAPI seq span
A special value representing the entire axis of an af::array.
af::copy
AFAPI void copy(array &dst, const array &src, const index &idx0, const index &idx1=span, const index &idx2=span, const index &idx3=span)
Copy the values of an input array based on index.
af::end
AFAPI int end
A special value representing the last value of an axis.
f32
@ f32
32-bit floating point values
Definition: defines.h:211