您获得分配的原因是因为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 的引用。因此,如果所有SubArrays 都存储在堆栈中,您将遇到这样的代码问题:
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::AbstractArray{T}, temp::AbstractArray{T}) 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,您也将避免分配。