使用数组视图时意外的内存分配 (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 的引用。因此,如果所有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::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】:

这至少适用于任意大小的 tempx,但仍然有 ~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)的主要内容,如果未能解决你的问题,请参考以下文章

Java静态内存与动态内存分配的解析

c语言数组在内存中是怎么分配的?

JAVA虚拟机内存分配与回收机制

理解JVM之内存分配以及分代思想实现

C++内存分配及变长数组的动态分配

java中创建数组时怎么分配内存?