Views as a way to improve code performance¶
This script discusses the use of views, a mechanism that allows you to access array elements without creating copies of them. The topics will be touched upon:
- the difference between a slice copy (slicing) and a view (view)
- use of macros
@view
and@views
and their differences.
In order to check the efficiency of using views - let's connect BenchmarkTools libraries
import Pkg; Pkg.add("BenchmarkTools")
Copying problem¶
When we use the syntax b = a[1:5]
, then b becomes a copy of the first five elements of a
, rather than "linking by address" to the elements of a
.
a = collect(1:10)
b = a[1:5] # [1, 2, 3, 4, 5]
println(pointer(a))
println(pointer(b))
b .= 0 # [0, 0, 0, 0, 0]
a' # как видим, матрица не поменялась, что и логично
To change our original vector, we are forced to do an extra action:
a[1:5] .= b[1:5]
a'
The view function¶
Views just allow us to use the familiar syntax, but to create not copies, but to access directly the "memory locations" of arrays.
To do this, you can use the function view
a = collect(1:10000)
# '÷' не то же, что и '/' (÷ = div())
view_of_a = view(a,1:length(a)÷2) # end здесь не сработает
view_of_a .= 0
a
pointer(view_of_a) == pointer(a)
Let's make sure that using views allows us to avoid allocating extra memory for copies by using @allocated
, which shows the number of bytes allocated.
println(@allocated (subarray_of_a = a[1:end÷2]))
println(@allocated (view_of_a = view(a,1:length(a)÷2)))
@view¶
But using the view
function does not fulfil the above statement about the "familiar interface" because we could not use, for example, the keyword end
.
To solve this problem we can use the macro @view
:
a = repeat(1:10,inner=3)
b = @view a[end-3:end]
b .= 0
a'
But the question may arise: why do we need unnecessary variables when we can just do a
a = repeat(1:10,inner=3)
a[end-3,3] .= 0
The answer to this can be formulated as follows:
Representations are needed as a union of efficient use of resources and preserving code readability.
Suppose there is a task to output and calculate the sum of triplet elements.
for i in 0:(length(a)÷3-1)
println("sum of $(a[3i+1:3i+3]) -> $(sum(a[3i+1:3i+3]))")")
end
You can see that there are repeating elements, and it is also easy to make a mistake in one of the indexings.
for i in 0:(length(a)÷3-1)
triplet = a[3i+1:3i+3]
println("sum of $triplet -> $(sum(triplet))")
end
In addition, we can see that a function using views will allocate much less memory and run much faster.
For this purpose, we will use the macro @btime
, which shows the execution time of the function and the memory allocated by running the function several times and averaging the values.
(We have removed the output to the console from the functions to avoid clogging the console during multiple function calls).
using BenchmarkTools
function tripletssum_subarray(v)
for i in 0:(length(v)÷3-1)
triplet = v[3i+1:3i+3]
end
end
function tripletssum_view(v)
for i in 0:(length(v)÷3-1)
triplet = @view v[3i+1:3i+3]
end
end
a = repeat(1:10000,inner=3)
println(@btime tripletssum_subarray(a))
println(@btime tripletssum_view(a))
@views¶
Let's consider the following example
Pkg.add("LinearAlgebra")
using LinearAlgebra
@btime dot( a[1:end÷2], a[end÷2+1:end])
And it would seem that we know how we can improve this code:
try
# ОСНОВНОЙ КОД
#------------------------------------------------------
@btime dot(@view a[1:end÷2], @view a[end÷2+1:end])
#------------------------------------------------------
# ОБРАБОТКА ИСКЛЮЧЕНИЯ
catch e
io = IOBuffer();
showerror(io, e)
error_msg = String(take!(io))
end
The error says that we are not using the macro correctly @view
.
Although our expression a[1:end÷2]
seems to match the expression A[...]
.
The problem is that we misused the macro.
In order to correct this situation, we put the vectors to which we want to apply the representation into brackets:
@btime dot(@view(a[1:end÷2]) ,@view(a[end÷2+1:end]))
But in order not to write @view
for each slicing we can use the macro @views
@btime @views dot((a[1:end÷2]), (a[end÷2+1:end]))
@views
can be inserted before the function definition, so that slices inside the function will be performed using views.
@views function tripletssum_views(v)
for i in 0:(length(v)÷3-1)
triplet = v[3i+1:3i+3]
end
end
a = repeat(1:10000,inner=3)
println(@btime tripletssum_views(a))
In what cases should we use views?¶
Views should be used where:
- it improves readability
- it affects productivity
- you understand the difference between working with the copy and the original
a = rand(1000)
println(@allocated sum(a))
println(@allocated sum(a[1:end]))
println(@allocated sum(copy(a[1:end])))
println(@allocated sum(@view a[1:end]))
import Pkg; Pkg.add(["OrdinaryDiffEq","Plots"])
using OrdinaryDiffEq
using Plots
gr()
function lotka(du, u, p, t)
du[1] = p[1] * u[1] - p[2] * u[1] * u[2]
du[2] = p[4] * u[1] * u[2] - p[3] * u[2]
end
α = 1; β = 0.01; γ = 1; δ = 0.02;
p = [α, β, γ, δ]
tspan = (0.0, 6.5)
u0 = [50; 50]
prob = ODEProblem(lotka, u0, tspan, p)
sol = solve(prob,saveat=0.001)
The time dependencies of the variables $x(t)$ and $y(t)$ will be used to draw the graphs.
plot(sol)
But if we want to draw the dependence $y(x)$, we will have to use the slices sol[1,:]
and sol[2,:]
. Which reminds us of our above-mentioned problem.
@btime plot(sol[1,:],sol[2,:])
Which we can now solve using views:
@btime @views plot(sol[1,:],sol[2,:])
Conclusions¶
Having learnt about the concept of views, practical ways to improve the performance of functions that do not require any significant changes to the code have been considered.