AnvlArrays are subset with [ and updated
with [<-, much like R arrays. Most differences to base R
come from two properties of compiled code: the shape of every result
must be known before the program runs (see the Static Shape Restriction article), and the
compiled program cannot throw errors. This article goes through what
that means for selecting and updating elements.
## AnvlArray
## 1
## 2
## 3
## 4
## 5
## 6
## 7
## 8
## 9
## 10
## [ CPUi32{10} ]
Selecting Elements
A single index selects one element and drops the axis, just like in R:
x[2]## AnvlArray
## 2
## [ CPUi32{} ]
To select several elements, pass the indices as an R array.
arr() is a shorthand for this,
e.g. arr(2, 4, 6) is the same as
array(c(2, 4, 6)):
x[arr(2, 4, 6)]## AnvlArray
## 2
## 4
## 6
## [ CPUi32{3} ]
A plain vector such as c(2, 4, 6) is not accepted,
because it would be ambiguous for a single index: should
x[c(2)] drop the axis, like x[2], or keep it?
With arrays, the answer is always clear, so {anvl} needs no
drop argument: x[2] drops the axis and
x[arr(2)] keeps it.
x[arr(2)]## AnvlArray
## 2
## [ CPUi32{1} ]
Ranges work as in R, including ranges that count down, and an empty subscript selects everything:
x[2:5]## AnvlArray
## 2
## 3
## 4
## 5
## [ CPUi32{4} ]
x[5:2]## AnvlArray
## 5
## 4
## 3
## 2
## [ CPUi32{4} ]
x[]## AnvlArray
## 1
## 2
## 3
## 4
## 5
## 6
## 7
## 8
## 9
## 10
## [ CPUi32{10} ]
For an AnvlArray with several axes, there is one
subscript per axis, and they combine as in R. Axes that are left out at
the end select all of their elements.
m <- nv_matrix(1:12, nrow = 3, byrow = TRUE)
m## AnvlArray
## 1 2 3 4
## 5 6 7 8
## 9 10 11 12
## [ CPUi32{3,4} ]
m[1, ] # the first row## AnvlArray
## 1
## 2
## 3
## 4
## [ CPUi32{4} ]
m[arr(1, 3), 2:3] # rows 1 and 3, columns 2 and 3## AnvlArray
## 2 3
## 10 11
## [ CPUi32{2,2} ]
m[2] # also the second row, as the column subscript is left out## AnvlArray
## 5
## 6
## 7
## 8
## [ CPUi32{4} ]
Indices Given as AnvlArrays
So far, all indices were R values. We call such a subset
static, because it does not depend on any
AnvlArray: its values are known in advance, so {anvl} can
check them before anything runs, e.g. whether an index is out of range
(see indices outside of the
AnvlArray below). An index can also be an
AnvlArray, e.g. one that is itself the result of a
computation. Such a subset is dynamic: its values are only
known once the computation runs, so these checks are not possible.
Below, the position of the largest element is used to select it:
v <- nv_array(c(3L, 9L, 4L, 1L))
v[nv_which_max(v)]## AnvlArray
## 9
## [ CPUi32{} ]
As with R values, a scalar index drops the axis, while an
AnvlArray with one axis keeps it and can select several
elements:
x[nv_array(2L)]## AnvlArray
## 2
## [ CPUi32{1} ]
## AnvlArray
## 2
## 4
## 6
## [ CPUi32{3} ]
Because the shape of the result must be known in advance, not every kind of subscript can be dynamic:
| Subscript | Known Value | Runtimew Value |
|---|---|---|
| Single index | Yes | Yes |
| Several indices | Yes | Yes |
| Range | Yes | No |
| Logical mask | Yes | Outside of jit()
|
The size of a range a:b, and therefore the shape of the
result, would not be known in advance if a or
b were dynamic. For the same reason, a logical mask
computed from the inputs of a jitted function, such as
x[x > 5], is not supported (see logical masks below). Negative indices, which
exclude elements in R, are not supported either.
Indices Outside of the AnvlArray
In a static subset, an index that is outside of the
AnvlArray is an error:
x[11]## Error in `parse_subset_spec()`:
## ! The index 11 is out of bounds for axis 1.
## ✖ Axis 1 has size 10, so indices must be between 1 and 10.
In a dynamic subset, the index only has a value once the computation runs, when errors can no longer be thrown (see compiled code cannot throw errors). Instead, the index is moved to the nearest valid one, so it is worth being careful with such indices to avoid bugs:
## AnvlArray
## 1
## 10
## [ CPUi32{2} ]
Logical Masks
A logical mask selects the elements at the TRUE
positions. Like indices, a mask must be an array – a plain logical
vector is rejected – so use arr() to write one out. A mask
for a single axis must have as many elements as the size of that axis,
and never drops the axis:
m[arr(TRUE, FALSE, TRUE), ]## AnvlArray
## 1 2 3 4
## 9 10 11 12
## [ CPUi32{2,4} ]
m[, arr(FALSE, TRUE, TRUE, FALSE)]## AnvlArray
## 2 3
## 6 7
## 10 11
## [ CPUi32{3,2} ]
A mask that has the same shape as the AnvlArray and is
the only subscript selects across all axes, and the result has a single
axis. This is the familiar x[x > 6] idiom:
m[m > 6]## AnvlArray
## 9
## 10
## 7
## 11
## 8
## 12
## [ CPUi32{6} ]
The elements come back in the same order as in R, i.e. column-major.
A single subscript that is not shaped like the whole
AnvlArray still refers to the first axis, as everywhere
else in {anvl}:
m[arr(TRUE, FALSE, TRUE)]## AnvlArray
## 1 2 3 4
## 9 10 11 12
## [ CPUi32{2,4} ]
This differs from base R, where a single subscript indexes the flattened matrix and a shorter mask is recycled, so the same call picks elements from all columns:
## [1] 1 9 2 10 3 11 4 12
{anvl} keeps one rule instead: x[i] always means
x[i, ], whether i is an index, a range,
several indices, or a mask. Only a mask of the full shape selects across
all axes, since it cannot be read as a subset of one axis. An
all-FALSE mask selects nothing and produces an
AnvlArray with no elements:
m[m > 100]## AnvlArray
## [ CPUi32{0} ]
The number of elements a mask selects depends on its values, so
inside jit() these must be known in advance. That is the
case for a mask of R values, and for an AnvlArray that the
function creates or closes over, because neither depends on the
inputs:
## AnvlArray
## 1 2 3 4
## 9 10 11 12
## [ CPUi32{2,4} ]
jit(function(m) m[row_mask, ])(m)## AnvlArray
## 1 2 3 4
## 9 10 11 12
## [ CPUi32{2,4} ]
A mask computed from the inputs only has values once the program runs, so it is an error:
jit(function(m) m[m > 6])(m)## Error in `as_r_mask()`:
## ! Boolean masks that depend on the inputs of a jitted function are not
## supported.
## ✖ The number of selected elements, and hence the output shape, depends on the
## data.
## ℹ Use a mask whose values are known at compile time: an R logical array such as
## `arr(TRUE, FALSE, TRUE)`, or an array created in or closed over by the
## function.
The masking
pattern section of the Static Shape Restriction article shows how to
get the same results inside jit(),
e.g. sum(x[x > 0]).
Updating Subsets
Subset assignment uses the same subscripts as selection. The new value must either have the shape of the selected subset, or be a scalar, which is then used for every selected element:
m[, 3] <- nv_array(-(1:3))
m## AnvlArray
## 1 2 -1 4
## 5 6 -2 8
## 9 10 -3 12
## [ CPUi32{3,4} ]
m[1, ] <- 0L
m## AnvlArray
## 0 0 0 0
## 5 6 -2 8
## 9 10 -3 12
## [ CPUi32{3,4} ]
A mask works as well, and the result keeps the shape of the
AnvlArray:
m[m > 6] <- 0L
m## AnvlArray
## 0 0 0 0
## 5 6 -2 0
## 0 0 -3 0
## [ CPUi32{3,4} ]
The value must also fit the data type of the AnvlArray.
This is different from base R, which changes the type of the object that
is assigned into when the value does not fit, e.g. an integer vector
becomes a double vector:
r_int <- 1:3
r_int[1] <- 0.5
r_int## [1] 0.5 2.0 3.0
typeof(r_int)## [1] "double"
{anvl} never changes the data type of the AnvlArray that
is being updated, so the same assignment is an error:
y <- nv_array(1:3)
y[1] <- 0.5## Error:
## ! Cannot bring `value` to data type "i32".
## ✖ It is an R double, which is only ever built at a data type of its own
## category: a double becomes a float, an integer an integer, a logical a
## "bool".
## ℹ Write it in the target's category (e.g. `0L` for an integer data type), or
## convert it with `nv_convert()`.
To store non-integer values, convert the AnvlArray to a
float data type first, e.g. with nv_convert(y, "f32").
Writes to dynamic indices outside of the AnvlArray are
silently ignored, for the same reason as when selecting (see indices outside of the
AnvlArray). Below, only the elements 1 and 3 are
updated:
## AnvlArray
## -1
## 2
## -3
## 4
## 5
## [ CPUi32{5} ]
When the same element is written several times, it is not specified which of the values ends up in it, and this can differ between devices, e.g. between the CPU and a GPU:
## AnvlArray
## 30
## 2
## 3
## 4
## 5
## [ CPUi32{5} ]
In-place Updates
Outside of jit(), y[i] <- value creates
a new AnvlArray, even where R would update y
in place. This is because {anvl} cannot know whether another R variable
still refers to the same AnvlArray, which would then change
as well. To avoid the copy, pass inplace = TRUE among the
subscripts, which writes the update into the memory of
y:
y <- nv_array(1:5)
y[2:3, inplace = TRUE] <- 0L
y## AnvlArray
## 1
## 0
## 0
## 4
## 5
## [ CPUi32{5} ]
nv_subset_assign() takes the same argument:
nv_subset_assign(y, 2:3, value = 0L, inplace = TRUE).
This consumes the original AnvlArray: its memory is donated to the result, so any other
R variable that referred to it can no longer be used afterwards.
y <- nv_array(1:5)
z <- y
y[1, inplace = TRUE] <- -1L
z## AnvlArray
## Error:
## ! called on deleted or donated buffer
Inside a jitted function, inplace = TRUE is an error.
There, z would stay valid, so the same code would behave
differently in eager and in jit mode. The compiler avoids unnecessary
copies inside jit() anyway, so inplace is not
needed there.