使用数组视图时出现意外的内存分配(julia)

Kir*_*ar. 3 arrays performance allocation julia

我正在尝试在数组X中搜索所需的模式(变量模板).模板的长度为9.

我做的事情如下:

function check_alloc{T <: ZeroOne}(x :: AbstractArray{T}, temp :: AbstractArray{T})
    s = 0
    for i in 1 : 1000
        myView = view(x, i : i + 9)
        if myView == temp
            s += 1
        end
    end
    return s
end
Run Code Online (Sandbox Code Playgroud)

并在此短循环中获得意外的内存分配(46 KB).为什么会发生这种情况?如何防止内存分配和性能下降?

tho*_*oly 15

你获得分配的原因是因为view(A, i:i+9)创建了一个名为a的小对象SubArray.这只是一个"包装器",它实际上存储了一个引用A和(i:i+9)传入的索引.因为包装器很小(一维对象大约40个字节),所以存储它有两个合理的选择:堆栈或堆栈."分配"仅指堆内存,因此如果Julia可以将包装器存储在堆栈上,它将报告没有分配(并且也会更快).

不幸的是,SubArray目前一些对象(截至2017年底)必须存储在堆上.原因是因为Julia是一种垃圾收集语言,这意味着如果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
Run Code Online (Sandbox Code Playgroud)

因为在打电话后create不再使用,所以不是"保护" .风险在于,除非防止被垃圾收集,否则调用可能最终释放与之关联的内存(从而破坏了对条目本身的任何使用,因为它依赖于此).但是目前,堆栈分配的变量无法保护堆分配的内存:垃圾收集器只扫描堆上的变量.AgetfirstAgcAvvAvA

您可以使用原始功能观察此操作,通过删除(无关,用于这些目的)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)
Run Code Online (Sandbox Code Playgroud)

从第一个(带a = collect(1:1010)),你得到

julia> @time check_alloc(a, b)
  0.000022 seconds (1.00 k allocations: 47.031 KiB)
Run Code Online (Sandbox Code Playgroud)

(注意这是每次迭代大约47个字节,与SubArray包装器的大小一致)但是从a = 1:1010你得到的第二个(和)

julia> @time check_alloc(a, b)
  0.000020 seconds (4 allocations: 160 bytes)
Run Code Online (Sandbox Code Playgroud)

这个问题有一个"明显的"修复:更改垃圾收集器,以便堆栈分配的变量可以保护堆分配的内存.那将在某一天发生,但这是一个非常复杂的操作,以正确支持.所以现在,规则是任何包含对堆分配内存的引用的对象都必须存储在堆上.

有一个最后的微妙之处:Julia的编译器非常聪明,并且在某些情况下省略了SubArray包装器的创建(基本上,它以一种使用父数组对象和索引的方式重写代码,因此它永远不需要包装器本身) .为了实现这一点,Julia必须能够将任何函数调用内联到创建该函数的函数中view.不幸的是,这==对于编译器来说有点太大了,不愿意内联它.如果您手动写出将要执行的操作,那么编译器将忽略该操作view,您也将避免分配.