Engee 文档
Notebook

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

此脚本讨论了视图的使用,这是一种允许访问数组元素而不创建它们的副本的机制。
主题将涵盖:
-切片副本(切片)和视图(视图)之间的区别
-宏的使用 @view@views 和他们的差异。

为了测试使用视图的有效性,我们将连接基准库。

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 它与上述关于"熟悉的界面"的声明不符,因为我们无法使用关键字 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=重复(1:10,内=3) a[结束-3,3].= 0
对此的答案可以制定如下:

视图是有效利用资源和保持代码可读性的组合。

假设有一个任务输出并计算三元组元素的总和。

``'茱莉亚
对于i in0:(长度(a)≈3-1)
println("总和 $(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

@意见

考虑以下示例

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[...].

问题是我们[错误地使用了宏](https://engee.com/helpcenter/stable/ru/julia/manual/metaprogramming.html#вызов-макроса )。

为了解决这种情况,我们将要应用表示的向量放在括号中。:

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]

绘制图形时,将使用时间因变量。 .

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

但是如果我们要绘制一个依赖 然后我们将不得不使用切片。 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]:

结论

在熟悉了表示的概念之后,考虑了改进不需要对代码进行任何重大更改的函数性能的实用方法。