Repurposing memory fragments of a large arrays

I have some code, which allocates a larger array, which I store in a named tuple along with other pre-allocated arrays. To save memory, I would like to re-purpose this array tmp_cpx to “host” two smaller arrays, since the large array is not used while the two small ones are.
Using code like this achieves the purpose:

sarr1 = reshape(view(tmp_cpx, 1:prod(imsz)), imsz)
sarr2 = reshape(view(tmp_cpx, prod(imsz)+1:2*prod(imsz)), imsz) 

BUT here comes the probelem:
Many routines expect an ordinary array and cannot deal with the resulting type, e.g.:
256×256 reshape(view(::Vector{ComplexF32}, 1:65536), 256, 256)
If you, for example apply an fft_plan generated with fft_plan!, it just silently fails but does not perform the in-place fft, one would hope for.
Is there a way to wrap a fraction of an array in such a way, that fft_plan!, and other functions can work with it? It sound like, one simply needs to exchange the memory pointer inside the array structure, but I am unsure how to achieve such a hack in Julia.

There is a nasty hack available. It involves the following.

  1. Obtaining a pointer from the reshaped view.
  2. Wrapping it into an Array
julia> tmp_cpx = Vector{ComplexF32}(undef, 256*256*2);

julia> sarr1 = reshape(view(tmp_cpx, 1:256*256), (256, 256));

julia> typeof(sarr1)
Base.ReshapedArray{ComplexF32, 2, SubArray{ComplexF32, 1, Vector{ComplexF32}, Tuple{UnitRange{Int64}}, true}, Tuple{}}

julia> pointer(sarr1)
Ptr{ComplexF32} @0x00000000088b8300

julia> sarr1_wrapped = unsafe_wrap(Array, pointer(sarr1), (256, 256); own=false)

julia> typeof(sarr1_wrapped)
Matrix{ComplexF32} (alias for Array{Complex{Float32}, 2})

The major thing one needs to be careful about is making sure that sarr1 is not garbage collected. You should probably have all the pointer calls within a GC.@preserve.

GC.@preserve sarr1 begin
    sarr1_wrapped = unsafe_wrap(Array, pointer(sarr1), (256, 256);
    # Compute with sarr1_wrapped
1 Like

In general, this is going to be UB because it violates TBAA (Type Based Alias Analysis) (see here). Julia assumes that only one object can occupy one region of memory at a time. Creating new objects that then occupy that same region of memory causes exactly that aliasing, so is disallowed.


But he’s not reinterpreting the element type, I think?

I don’t think that matters in this case. If the “inner” allocations gets freed by the GC and the memory is reused for a different allocation (even though the memory is still alive through another object, the “outer vector”), you end up with a double-use/use-after-free.

Of course, it’s hard to say this definitively as a user of the language, because the memory model is still not documented

1 Like

In version 1.11 you will be able to do this with Base.wrap.

julia> let tmp_cpx = collect(1:10)
           mem = tmp_cpx.ref
           sarr1 = Base.wrap(Array, MemoryRef{Int}(mem, 1), 5)
           sarr2 = Base.wrap(Array, MemoryRef{Int}(mem, 6), 5)
           @info "" sarr1 sarr2
┌ Info: 
│   sarr1 =
│    5-element Vector{Int64}:
│     1
│     2
│     3
│     4
│     5
│   sarr2 =
│    5-element Vector{Int64}:
│      6
│      7
│      8
│      9
└     10
1 Like

Not great API support for this, Base.contiguous can’t even check reshapes, only subarrays.

This does not seem like a safe use to me :thinking: After all, it’s accessing internals of the array to construct the new arrays.

There should be a getter function for the memory of an array, but if I recall correctly there isn’t one currently.

It’s certainly safer than doing pointer gymnastics though.

With such a significant change in v1.11 to a, as you mentioned, not-much-documented aspect of Arrays, it’s hard to say what is or isn’t safe. It is however possible for 1 array type to encompass both allocations and views, that’s how NumPy arrays work. Not really sure if that should be emulated entirely though, non-contiguous views can’t really be treated the same way. Unlike Python’s slices, Julia’s type system does distinguish UnitRanges and StepRanges, so that difference in syntax could help.

From my experience in the SciPy world, I will say it can easily cost too much memory to aggressive make views instead of allocating. Sounds paradoxical and probably doesn’t apply to this carefully managed example, but a few small views can keep a much larger parent array alive too long. Setting up points to copy data isn’t too bad though, and there were easy ways to distinguish views and arrays.