【问题标题】:Unexpected memory allocation when using array views (julia)使用数组视图时意外的内存分配 (julia)
【发布时间】:2018-05-15 09:48:07
【问题描述】:

我正在尝试在数组 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)。为什么会发生这种情况?如何防止内存分配和性能下降?

【问题讨论】:

  • 什么是ZeroOne?此外,您说您正在搜索的模式的长度为 9,但您正在创建一个长度为 10 的视图 i:i+9
  • 这不是关于view,而是==操作(你可以注释掉它并查看@time)。您可以手动重写此比较或查看@edit (==)(AbstractArray[], AbstractArray[])
  • ZeroOne 是联合{Bool, Int8, UInt8}。视图创建确实不需要内存,但是为什么(==)操作这么慢?为什么需要这么多内存?
  • == 不会创建临时数组,但 .== 会。

标签: arrays performance julia allocation


【解决方案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 的引用。因此,如果所有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&lt;: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,您也将避免分配。

【讨论】:

【解决方案2】:

这至少适用于任意大小的 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 在 cmets 中所建议的,人们可以本着这种精神做一些事情:

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)

【讨论】:

  • 非常感谢!我对这种情况完全感到困惑。 (==) 操作真的需要分配这么多内存吗?
【解决方案3】:

从 Julia 1.7.0(可能更早)开始,@carstenbauer 的第一个带有视图的代码不再分配(在堆上):

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

【讨论】:

    猜你喜欢
    • 2021-03-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多