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

时间:2017-12-01 09:58:55

标签: arrays performance julia allocation

我试图在数组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

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

2 个答案:

答案 0 :(得分:11)

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

不幸的是,当前(截至2017年底)的一些SubArray对象必须存储在堆上。原因是因为Julia是一种garbage-collected语言,这意味着如果A是一个不再使用的堆分配对象,那么A可能会从内存中释放出来。关键点在于:目前,只有当这些变量存储在堆上时才会计算来自其他变量的A的引用。因此,如果所有SubArray都存储在堆栈中,那么对于这样的代码会有问题:

function create()
    A = rand(1000)
    getfirst(view(A, 1:10))
end

function getfirst(v)
    gc()   # this triggers garbage collection
    first(v)
end

由于create在调用A后没有再次使用getfirst,因此它不是“保护”A。风险在于gc调用可能最终释放与A相关联的内存(从而打破v本身条目的任何使用,因为v依赖于{A 1}}),除非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并且您也将避免分配。

答案 1 :(得分:0)

这至少适用于任意大小的tempx,但仍然有~KB分配。

function check_alloc{T}(x :: AbstractArray{T}, temp :: AbstractArray{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

编辑:正如@Sairus在评论中所建议的那样,人们可以本着这样的精神做点什么:

function check_alloc2{T}(x :: AbstractArray{T}, temp :: AbstractArray{T})
    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)