Generic expressions are represented by <something> (e.g., <function> or <operator>).
This is just notation, and the symbols < and > should not be misconstrued as Julia's syntax.
PAGE LAYOUT
If you want to adjust the size of the font, zoom in or out on pages by respectively using Ctrl++ and Ctrl+-. The layout will adjust automatically.
LINKS TO SECTIONS
To quickly share a link to a specific section, simply hover over the title and click on it. The link will be automatically copied to your clipboard, ready to be pasted.
KEYBOARD SHORTCUTS
Action
Keyboard Shortcut
Previous Section
Ctrl + 🠘
Next Section
Ctrl + 🠚
List of Sections
Ctrl + z
List of Subsections
Ctrl + x
Close Any Popped Up Window (like this one)
Esc
Open All Codes and Outputs in a Post
Alt + 🠛
Close All Codes and Outputs in a Post
Alt + 🠙
TIME MEASUREMENT
When benchmarking, the equivalence of time measures is as follows.
This section concludes the coverage of preliminary concepts for studying mutations. The focus is now on the behavior of a vector's slice. This is defined as a subset of a vector's elements, formally represented by x[<indices>].
Importantly, slices act differently depending on how they're included in a statement, behaving as either:
copies of the original vector, thus creating a new object at a new memory address.
views of the original vector, where the original object and the slice share the same memory address.
In the following, we explain the distinction between copies and views in detail. Understanding it is crucial for mutating slices, as mutations can only occur when the slice references the original object. In contrast, if a slice acts as a copy, the parent object and the slice are unrelated, with changes to the slice having no impact on the original object.
Slices and the Assignment Operator
Vector mutation involves modifying slices through the operator =. For this to be possible, a prerequisite is that the slice references the original object. Nonetheless, the behavior of slices in assignments varies depending on their position within the expression.
Specifically, slices on the left-hand (LHS) side of=act as views. In this case, slices reference the original elements, thus allowing for the mutation of its parent object. In contrast, slices on the right-hand side (RHS) of=create a copy. Since copies point to a new object, any modification to the slice won't affect the original object.
The following code snippet demonstrates each behavior.
x = [4,5]
x[1] = 0 # 'x[1]' is a view and mutates 'x'
Output in REPL
julia>
x
2-element Vector{Int64}:
0
5
x = [4,5]
y = x[1] # 'y' is unrelated to 'x' because 'x[1]' is a copy
x[1] = 0 # it mutates 'x' but does NOT modify 'y'
Output in REPL
julia>
y
4
Aliasing vs Copy
Objects on the RHS of = are only treated as copies when it comes to slices, such as in statements y = x[<indices>]. Instead, if we insert the whole object x on the RHS of =, as in y = x, the operation creates an alias. In this case, y and x will reference the same object, and so any modification made to y will also be reflected in x.
x = [4,5]
y = x # the whole object (a view)
x[1] = 0 # it DOES modify 'y'
Output in REPL
julia>
y
2-element Vector{Int64}:
4
5
x = [4,5]
y = x[:] # a slice of the whole object (a copy)
x[1] = 0 # it does NOT modify 'y'
Output in REPL
julia>
y
2-element Vector{Int64}:
0
5
The Function 'view'
Identifying when slices act as copies or views is relevant for high performance, as views eliminate the overhead associated with memory allocations. Although this topic will be explored in Part II of the website, such considerations underscore the importance of distinguishing between copies and slices, beyond their use in assignments.
As a rule of thumb, slices typically default to creating copies. This is the case when, for instance, a slice is passed as a function argument or when used within an expression not involving an assignment. These scenarios are illustrated below.
x = [3,4,5]
#the following slices are all copies
log.(x[1:2])
x[1:2] .+ 2
[sum(x[:]) * a for a in 1:3]
(sum(x[1:2]) > 0) && true
In all these cases, transforming slices into views requires an explicit indication. To achieve this, you need to employ the function view. Its syntax is view(x, <indices>), where <indices> represent the subset of indices defining the slice. To demonstrate its usage, we revisit and compare the previous code snippet.
x = [3,4,5]
#we make explicit that we want views
log.(view(x,1:2))
view(x,1:2) .+ 2
[sum(view(x,:)) * a for a in 1:3]
(sum(view(x,:)) > 0) && true
x = [3,4,5]
#the following slices are all copies
log.(x[1:2])
x[1:2] .+ 2
[sum(x[:]) * a for a in 1:3]
(sum(x[1:2]) > 0) && true
These examples reveal the potential verbosity involved when view isn't sparsely used. To address this issue, Julia provides the macros @view and @views.
The @view macro is equivalent to view, allowing you to write @view x[1:2] instead view(x, 1:2). However, its advantages are somewhat limited: it saves only a few characters, and additionally necessarily requires parentheses when multiple slices are used (e.g., @view(x[1:2]) .+ @view(x[2:3])). In contrast, the @views macro significantly streamlines notation, by automatically converting every slice within an expression into a view.
x = [4,5,6]
# the following are all equivalent
y = view(x, 1:2) .+ view(x, 2:3)
y = @view(x[1:2]) .+ @view(x[2:3])
@views y = x[1:2] .+ x[2:3]
One of the most notable applications of @views is in functions. By placing @views at the beginning of a function, you automatically convert every slice within the function body and its arguments into a view.
@views function foo(x)
y = x[1:2] .+ x[2:3]
z = sum(x[:]) .+ sum(y)
return z
end
function foo(x)
y = @view(x[1:2]) .+ @view(x[2:3])
z = sum(@view x[:]) .+ sum(y)
return z
end