Combining reshape and view sometimes allocates

I have some complicated code that uses a combination of reshape() and view(), and in my complicated code both reshape() and view() allocates. Trying to reduce my code to an MRE, I came up with this:

function profile_allocs()
    x = randn(6)
    Profile.Allocs.clear()
    Profile.Allocs.@profile sample_rate=1.0 begin
        x = reshape(x, (3,2))
        sum(view(x, :))
    end
    Profile.Allocs.print()
end

On 1.11.9, this allocates:

julia> profile_allocs()
Overhead ╎ [+additional indent] Count File:Line; Function
=========================================================
  # ...
  ╎    ╎    ╎   32 REPL[2]:4; profile_allocs()
  ╎    ╎    ╎    32 @Profile/src/Allocs.jl:82; macro expansion
  ╎    ╎    ╎     32 REPL[2]:6; macro expansion
  ╎    ╎    ╎    ╎ 32 @Base/subarray.jl:216; view
  ╎    ╎    ╎    ╎  32 @Base/subarray.jl:156; _maybe_reshape_parent
  ╎    ╎    ╎    ╎   32 @Base/reshapedarray.jl:152; reshape
  ╎    ╎    ╎    ╎    32 @Base/reshapedarray.jl:121; reshape
32╎    ╎    ╎    ╎     32 @Base/reshapedarray.jl:60; reshape
Total snapshots: 1
Total bytes: 32

On 1.12.5, this does not allocate:

julia> profile_allocs()
Overhead ╎ [+additional indent] Count File:Line  Function
=========================================================
┌ Warning: There were no samples collected.
│ Run your program longer (perhaps by running it multiple times),
│ or adjust the frequency of samples to record every event with
│ the `sample_rate=1.0` kwarg.
└ @ Profile.Allocs ~/.julia/juliaup/julia-1.12.5+0.aarch64.apple.darwin14/Julia-1.12.app/Contents/Resources/julia/share/julia/stdlib/v1.12/Profile/src/Allocs.jl:226

I guess my primary objective here is trying to understand why v1.11 allocates, but anything that sheds some light on Julia’s current allocation behaviour will probably be useful.

1 Like

This looks like reshape(::Array, ...) is slow and allocates · Issue #36313 · JuliaLang/julia · GitHub, but with 1.12’s extra smarts about escape analysis saving it from getting there.

Basically, it’s not allocating the whole new array. It’s still a view. It just needs an extra heap-allocated header that describes the new dimensions.

3 Likes

Ah, so the problem is that Array’s implementation is roughly equivalent to

mutable struct Array{T, N}
    data::Memory{T}
    dims::NTuple{N, Int}
end

and therefore reshape() technically must always allocate a new such Array object. The “miracle” is that most of the time it doesn’t, because the compiler can prove that this new Array object has a bounded lifetime and therefore doesn’t need heap allocation. That makes perfect sense, I was just confused by the fact that reshape() doesn’t allocate most of the time. That made me think that the above example must be exceptionally bad in some ways, but really the point is that most uses of reshape() are exceptionally “good” for not needing allocation due to successful escape analysis.