使用数组视图时意外的内存分配 (julia)
Posted
技术标签:
【中文标题】使用数组视图时意外的内存分配 (julia)【英文标题】:Unexpected memory allocation when using array views (julia) 【发布时间】:2018-05-15 09:48:07 【问题描述】:我正在尝试在数组 X 中搜索所需的模式(变量模板)。模板的长度为 9。
我正在做类似的事情:
function check_allocT <: ZeroOne(x :: AbstractArrayT, temp :: AbstractArrayT)
s = 0
for i in 1 : 1000
myView = view(x, i : i + 9)
if myView == temp
s += 1
end
end
return s
end
并在这个短循环中获得意外的内存分配(46 KB)。为什么会发生这种情况?如何防止内存分配和性能下降?
【问题讨论】:
什么是ZeroOne
?此外,您说您正在搜索的模式的长度为 9,但您正在创建一个长度为 10 的视图 i:i+9
。
这不是关于view
,而是==
操作(你可以注释掉它并查看@time
)。您可以手动重写此比较或查看@edit (==)(AbstractArray[], AbstractArray[])
或许也可以看看***.com/questions/36346005/…
ZeroOne 是联合Bool, Int8, UInt8。视图创建确实不需要内存,但是为什么(==)操作这么慢?为什么需要这么多内存?
==
不会创建临时数组,但 .==
会。
【参考方案1】:
您获得分配的原因是因为view(A, i:i+9)
创建了一个名为SubArray
的小对象。这只是一个“包装器”,它本质上存储了对 A
的引用和您传入的索引 (i:i+9
)。因为包装器很小(一维对象约为 40 字节),所以有两种合理的选择来存储它:on the stack or on the heap。 “分配”仅指堆内存,因此如果 Julia 可以将包装器存储在堆栈上,它将报告没有分配(而且速度也会更快)。
不幸的是,目前(截至 2017 年底)一些 SubArray
对象必须存储在堆上。原因是因为 Julia 是一种garbage-collected 语言,这意味着如果A
是一个不再使用的堆分配对象,那么A
可能会从内存中释放。关键点是:目前,只有当这些变量存储在堆上时,才会计算从其他变量对A
的引用。因此,如果所有SubArray
s 都存储在堆栈中,您将遇到这样的代码问题:
function create()
A = rand(1000)
getfirst(view(A, 1:10))
end
function getfirst(v)
gc() # this triggers garbage collection
first(v)
end
因为在调用getfirst
之后create
不再使用A
,所以它不是“保护”A
。风险在于gc
调用最终可能会释放与A
关联的内存(因此会破坏v
本身中条目的任何使用,因为v
依赖于A
),除非有v
保护A
不被垃圾收集。但目前,堆栈分配的变量无法保护堆分配的内存:垃圾收集器只扫描堆上的变量。
您可以使用原始函数观看此操作,通过删除(与这些目的无关的)T<:ZeroOne
并允许任何 T
进行修改以稍微减少限制。
function check_alloc(x::AbstractArrayT, temp::AbstractArrayT) where T
s = 0
for i in 1 : 1000
myView = view(x, i : i + 9)
if myView == temp
s += 1
end
end
return s
end
a = collect(1:1010); # this uses heap-allocated memory
b = collect(1:10);
@time check_alloc(a, b); # ignore the first due to JIT-compilation
@time check_alloc(a, b)
a = 1:1010 # this doesn't require heap-allocated memory
@time check_alloc(a, b); # ignore due to JIT-compilation
@time check_alloc(a, b)
从第一个(a = collect(1:1010)
),你得到
julia> @time check_alloc(a, b)
0.000022 seconds (1.00 k allocations: 47.031 KiB)
(请注意,每次迭代约为 47 个字节,与 SubArray
包装器的大小一致)但从第二个(使用 a = 1:1010
)开始
julia> @time check_alloc(a, b)
0.000020 seconds (4 allocations: 160 bytes)
对这个问题有一个“明显”的解决方法:更改垃圾收集器,以便堆栈分配的变量可以保护堆分配的内存。这总有一天会发生,但要正确支持这是一项极其复杂的操作。所以现在,规则是任何包含对堆分配内存的引用的对象都必须存储在堆上。
最后一个微妙之处:Julia 的编译器非常聪明,在某些情况下省略了 SubArray
包装器的创建(基本上,它以分别使用父数组对象和索引的方式重写您的代码,以便它永远不需要包装器本身)。为此,Julia 必须能够inline 任何函数调用到创建view
的函数中。不幸的是,这里的==
有点太大,编译器不愿意内联它。如果您手动写出将要执行的操作,那么编译器将忽略view
,您也将避免分配。
【讨论】:
感谢您深入分享幕后发生的事情。暴露自 - Too many allocations when indexing with slices。【参考方案2】:这至少适用于任意大小的 temp
和 x
,但仍然有 ~KB 分配。
function check_allocT(x :: AbstractArrayT, temp :: AbstractArrayT)
s = 0
pl = length(temp)
for i in 1:length(x)-pl+1
@views if x[i:i+pl-1] == temp
s += 1
end
end
return s
end
编辑:正如@Sairus 在 cmets 中所建议的,人们可以本着这种精神做一些事情:
function check_alloc2T(x :: AbstractArrayT, temp :: AbstractArrayT)
s = 0
pl = length(temp)
plr = 1:pl
for i in 1:length(x)-pl+1
same = true
for k in plr
@inbounds if x[i+k-1] != temp[k]
same = false
break
end
end
if same
s+=1
end
end
return s
end
这没有分配:
julia> using BenchmarkTools
julia> a = collect(1:1000);
julia> b = collect(5:12);
julia> @btime check_alloc2($a,$b);
1.195 μs (0 allocations: 0 bytes)
【讨论】:
非常感谢!我对这种情况完全感到困惑。 (==) 操作真的需要分配这么多内存吗?【参考方案3】:从 Julia 1.7.0(可能更早)开始,@carstenbauer 的第一个带有视图的代码不再分配(在堆上):
function check_alloc(x :: AbstractArrayT, temp :: AbstractArrayT) where T
s = 0
pl = length(temp)
for i in 1:length(x)-pl+1
@views if x[i:i+pl-1] == temp
s += 1
end
end
return s
end
using BenchmarkTools
a = collect(1:1000);
b = collect(5:12);
@btime check_alloc($a,$b);
# returns
# 8.495 μs (0 allocations: 0 bytes)
【讨论】:
以上是关于使用数组视图时意外的内存分配 (julia)的主要内容,如果未能解决你的问题,请参考以下文章