Engee 文档
Notebook

视图是提高代码性能的一种方法

本脚本将讨论视图的使用,视图是一种无需创建副本即可访问数组元素的机制。 将涉及以下主题

  • 片拷贝(slicing)与视图(view)的区别
  • @view@views 的使用及其区别。

为了检查使用视图的效率--让我们连接 BenchmarkTools 库

In [ ]:
import Pkg; Pkg.add("BenchmarkTools")
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

复制问题

当我们使用b = a[1:5] 语法时,b 变成了a 前五个元素的副本,而不是 "按地址链接 "到a 的元素。

In [ ]:
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'                   # как видим, матрица не поменялась, что и логично
Ptr{Int64} @0x00007fb4fa4125f0
Ptr{Int64} @0x00007fb548a22980
Out[0]:
1×10 adjoint(::Vector{Int64}) with eltype Int64:
 1  2  3  4  5  6  7  8  9  10

为了改变原始矢量,我们不得不进行额外的操作:

In [ ]:
a[1:5] .= b[1:5]
a'
Out[0]:
1×10 adjoint(::Vector{Int64}) with eltype Int64:
 0  0  0  0  0  6  7  8  9  10

视图函数

视图允许我们使用熟悉的语法,但创建的不是副本,而是直接访问数组的 "内存位置"。 为此,您可以使用函数view

In [ ]:
a = collect(1:10000)
# '÷' не то же, что и '/' (÷ = div()) 
view_of_a = view(a,1:length(a)÷2) # end здесь не сработает
view_of_a .= 0
a
Out[0]:
10000-element Vector{Int64}:
     0
     0
     0
     0
     0
     0
     0
     0
     0
     0
     0
     0
     0
     ⋮
  9989
  9990
  9991
  9992
  9993
  9994
  9995
  9996
  9997
  9998
  9999
 10000
In [ ]:
pointer(view_of_a) == pointer(a)
Out[0]:
true

@allocated ,它可以显示已分配的字节数,从而确保使用视图可以避免为副本分配额外内存。

In [ ]:
println(@allocated (subarray_of_a = a[1:end÷2]))
println(@allocated (view_of_a = view(a,1:length(a)÷2)))
40112
112

@view

但使用view 函数并不符合上述关于 "熟悉的界面 "的说法,因为我们无法使用关键字end 等。 为了解决这个问题,我们可以使用宏@view

In [ ]:
a = repeat(1:10,inner=3)
b = @view a[end-3:end]
b .= 0
a'
Out[0]:
1×30 adjoint(::Vector{Int64}) with eltype Int64:
 1  1  1  2  2  2  3  3  3  4  4  4  5  …  7  7  7  8  8  8  9  9  0  0  0  0

但问题可能会出现:既然我们可以直接使用

a = repeat(1:10,inner=3)
a[end-3,3] .= 0

答案如下:

作为有效利用资源和保持代码可读性的结合体,我们需要表征。

假设有一项任务需要输出和计算三元组元素的总和。

``朱莉娅 for i in 0:(length(a)÷3-1) println("sum of$(a[3i+1:3i+3]) -> $(sum(a[3i+1:3i+3]))")") 结束


可以看出,这里有重复的元素,也很容易在某个索引中出错。

In [ ]:
for i in 0:(length(a)÷3-1)
    triplet = a[3i+1:3i+3]
    println("sum of $triplet -> $(sum(triplet))")
end
sum of [1, 1, 1] -> 3
sum of [2, 2, 2] -> 6
sum of [3, 3, 3] -> 9
sum of [4, 4, 4] -> 12
sum of [5, 5, 5] -> 15
sum of [6, 6, 6] -> 18
sum of [7, 7, 7] -> 21
sum of [8, 8, 8] -> 24
sum of [9, 9, 0] -> 18
sum of [0, 0, 0] -> 0

此外,我们还可以看到,使用视图的函数分配的内存要少得多,运行速度也要快得多。 为此,我们将使用宏@btime ,它可以通过多次运行函数并取平均值来显示函数的执行时间和分配的内存。

(我们删除了函数向控制台的输出,以避免在多次调用函数时堵塞控制台)。

In [ ]:
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))
  442.980 μs (10000 allocations: 781.25 KiB)
nothing
  11.152 μs (0 allocations: 0 bytes)
nothing

@views

让我们看看下面的示例

In [ ]:
Pkg.add("LinearAlgebra")
using LinearAlgebra
@btime dot( a[1:end÷2], a[end÷2+1:end])
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
  51.050 μs (13 allocations: 234.64 KiB)
Out[0]:
312575002500

我们似乎知道如何改进这段代码了:

In [ ]:
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
Out[0]:
"LoadError: ArgumentError: Invalid use of @view macro: argument must be a reference expression A[...].\nin expression starting at In[53]:4"

错误说明我们没有正确使用宏@view

虽然我们的表达式a[1:end÷2] 似乎与表达式A[...] 匹配。

问题在于我们误用了宏

为了纠正这种情况,我们把要应用表示法的向量放在括号中:

In [ ]:
  @btime dot(@view(a[1:end÷2]) ,@view(a[end÷2+1:end]))  
  4.153 μs (11 allocations: 272 bytes)
Out[0]:
312575002500

但为了避免每次切分都写@view ,我们可以使用宏@views

In [ ]:
  @btime @views dot((a[1:end÷2]), (a[end÷2+1:end]))  
  3.801 μs (11 allocations: 272 bytes)
Out[0]:
312575002500

@views 插入到函数定义之前,这样函数内部的分片将使用视图执行。

In [ ]:
@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))
  11.047 μs (0 allocations: 0 bytes)
nothing

什么情况下应该使用视图?

应在以下情况下使用视图

  • 提高可读性
  • 影响工作效率
  • 了解处理副本和原件之间的区别
In [ ]:
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]))
16
8192
17152
2824
In [ ]:
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)
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
Out[0]:
retcode: Success
Interpolation: 1st order linear
t: 6501-element Vector{Float64}:
 0.0
 0.001
 0.002
 0.003
 0.004
 0.005
 0.006
 0.007
 0.008
 0.009
 0.01
 0.011
 0.012
 ⋮
 6.489
 6.49
 6.491
 6.492
 6.493
 6.494
 6.495
 6.496
 6.497
 6.498
 6.499
 6.5
u: 6501-element Vector{Vector{Float64}}:
 [50.0, 50.0]
 [50.02500624847936, 50.000012502345534]
 [50.05002498979869, 50.00005001769738]
 [50.07505621775588, 50.00011255855343]
 [50.1000999261168, 50.000200137444956]
 [50.125156108615315, 50.00031276693659]
 [50.1502247589533, 50.00045045962633]
 [50.17530587080062, 50.000613228145575]
 [50.20039943779515, 50.00080108515906]
 [50.22550545354273, 50.001014043364925]
 [50.25062391161723, 50.00125211549466]
 [50.27575480556051, 50.001515314313124]
 [50.30089812888241, 50.00180365261858]
 ⋮
 [50.05479160200564, 49.98705194268289]
 [50.079830802201556, 49.987119920075195]
 [50.104882567590614, 49.987212877939285]
 [50.12994689359235, 49.987330827635944]
 [50.15502377561346, 49.98747378054721]
 [50.18011320904711, 49.98764174807645]
 [50.20521518927415, 49.98783474164827]
 [50.23032971166218, 49.988052772708585]
 [50.2554567715658, 49.98829585272456]
 [50.28059636432637, 49.98856399318467]
 [50.305748485272886, 49.98885720559867]
 [50.330913129720685, 49.98917550149757]

$x(t)$ 和$y(t)$ 变量的时间相关性将用于绘制图形。

In [ ]:
plot(sol)
Out[0]:

但如果我们要绘制$y(x)$ 的依赖关系,就必须使用切片sol[1,:]sol[2,:] 。这让我们想起了上述问题。

In [ ]:
@btime plot(sol[1,:],sol[2,:])
  582.602 μs (494 allocations: 451.46 KiB)
Out[0]:

现在我们可以使用视图来解决这个问题:

In [ ]:
@btime @views plot(sol[1,:],sol[2,:])
  488.246 μs (492 allocations: 102.46 KiB)
Out[0]:

结论

在了解了视图的概念之后,我们已经考虑了无需对代码进行重大修改即可提高函数性能的实用方法。