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:
import arrayfire as af
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
A = af.Array(data)
A = af.moddims(A, (4, 4))
ArrayFire is column-major so the resulting A array will look like this:
In Python, for a two-dimensional array like a matrix, you can access its first element by providing the indices 0, 0 within the indexing operator of the af.Array object.
A[0, 0] # Returns an array pointing to the first element
A[2, 3] # WARN: avoid doing this. Demo only
Note
Normally you want to avoid accessing individual elements of the array like this for performance reasons.
This is a warning note regarding accessing individual elements of arrays.
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.
ref0 = A[2, -1] # 14 second row last column
ref1 = A[2, -2] # 10 Second row, second to last(third) column
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[:, 2]
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, :] to the array
# Returns an array pointing to the second row
A[1, :]
You can use Python’s slicing notation to define a range when indexing in arrayfire. For example, if you want to get the first two columns of an array, you can access the array by specifying ‘:’ for the rows (to select all rows), and 0:2 for the columns (to select columns from index 0 to 1).
# Returns an array pointing to the first two columns
A[:, 0:2]
Indexing using af.Array
In Python with arrayfire, you can also index arrays using other af.Array objects. ArrayFire flattens the input and treats the elements inside the array as column major indices to index the original Array as 1D Array.
import arrayfire as af
x = af.randu((10, 10))
# indices 1, 3, 5
indices = af.range((3)) * 2 + 1
# returns entries with indices 1, 3, 5 of x.flat()
y = x[indices]
You can also index Arrays using boolean Arrays. ArrayFire will return an Array with length of the number of True
elements in the indexing arrays
and entries of in column major order of the elements that correspond to True
entries:
import arrayfire as af
# Creates a random array
x = af.randu((10, 10))
# returns an array with all the entries of x that contain values greater than 0.5
y = x[x > 0.5]
References and copies
All indexing operations in ArrayFire return af.Array objects, which are instances of the array_proxy class. These objects can either be newly created arrays or references to the original array, depending on the type of indexing operation applied to them
When an array is indexed using another af.Array , a new array is created instead of referencing the original data.
If an array was indexed using a scalar, sequential ‘0:2’ or 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.
reference = A[:, 1]
reference2 = A[0:3, 1]
reference3 = A[0:2, :]
The following code snippet shows some examples of indexing that will allocate new memory.
copy = A[2, :]
copy2 = A[1:3:2, :]
hidx = [0, 1, 2]
idx = af.Array(hidx)
copy3 = A[idx, :]
Even though the copy3 array references continuous memory in the original array, using an af.Array for indexing in ArrayFire results in the creation of a new array
Assignment
In Python with ArrayFire, assigning an af.Array replaces the array on the left-hand side of =
with the result from the right-hand side. This can lead to changes in type and shape compared to the original array. Notably, assignments do not update arrays previously referenced through indexing operations.
inputA = af.constant(3, (10, 10))
inputB = af.constant(2, (10, 10))
data = af.constant(1, (10, 10))
# Points to the second column of data. Does not allocate memory
ref = data[:, 1]
# This call does NOT update data. Memory allocated in matmul
ref = af.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[:, 2]
.
reference = A[:, 2]
A[:, 2] = 3.14
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:
ref = A[:, 2]
A[:, 2] = 3.14
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.
hidx = [4, 3, 4, 0]
hvals = [9.0, 8.0, 7.0, 6.0]
idx = af.Array(hidx)
vals = af.Array(hvals)
Member Functions
Check the Array Class for more details on other functions that the Array object provides.
Additional examples
See Assignment & Indexing operation on arrays for the full listing.
A = af.Array[1, 2, 3, 4, 5, 6, 7, 8, 9]
A = af.moddims(A, (3, 3))
# 1.0000 4.0000 7.0000
# 2.0000 5.0000 8.0000
# 3.0000 6.0000 9.0000
print(A[0, 0]) # first element
# 1.0000
print(A[0, 1]) # first row, second column
# 4.0000