Hi,
I have a 3D array (X::Array{Int8, 3}, size N x K x 2). How can I convert it to a Matrix (size N x K) of Vector such that new X will be X[i, j] = X[i, j, :] with very little new memory allocation.
Thanks,
Vishnu Raj
Hi,
I have a 3D array (X::Array{Int8, 3}, size N x K x 2). How can I convert it to a Matrix (size N x K) of Vector such that new X will be X[i, j] = X[i, j, :] with very little new memory allocation.
Thanks,
Vishnu Raj
Maybe:
map(CartesianIndices(axes(vol)[1:2])) do i
vol[Tuple(i)..., :]
end
You can also use views, if that works for your use case (view(vol, Tuple(i)..., :))
Btw, that sounds like a job for StaticArrays:
using StaticArrays
vol = rand(1000, 1000, 2)
function test(vol)
ax = axes(vol)
map(CartesianIndices((ax[1], ax[2]))) do i
x, y = Tuple(i)
SVector(ntuple(z-> vol[x, y, z], 2))
end
end
Which should allocate much less and be much more gc friendly ![]()
@sdanisch, that is pretty advanced code and hard to grasp ![]()
An alternative herein, using TensorCast.jl, which seems to use the same “black box technology” under the hood as StaticArrays, but thanks to a very logical and accessible syntax, one can get equivalent results in a more user-friendly way:
using TensorCast
@cast out2[i,j]{k} := vol[i,j,k]
NB: curly brackets allow different interpretation slices of the same memory, and assignment := returns views
Note that if you can cheat a bit (don’t need a Matrix, but an AbstractMatrix with the same shape will do), you can use essentially no memory at all with StructArrays + StaticArrays
julia> using StaticArrays, StructArrays, BenchmarkTools
julia> function to_structarray(X::AbstractArray{T, N}) where {T, N}
return StructArray{SVector{2, T}}(X, dims=N)
end
to_structarray (generic function with 1 method)
julia> X = rand(Int8, 1000, 1000, 2);
julia> @btime S = to_structarray($X)
540.741 ns (8 allocations: 368 bytes)
1000Ă—1000 StructArray(view(::Array{Int8, 3}, :, :, 1), view(::Array{Int8, 3}, :, :, 2)) with eltype SVector{2, Int8}:
[-122, 88] [95, 113] [-66, 65] [37, -3] … [73, -90] [-50, -98] [37, 5] [78, -51]
[-126, -16] [-54, 5] [94, 38] [78, 84] [-23, -34] [24, -15] [102, 50] [-122, -62]
⋮ ⋱
[-76, -88] [78, 20] [91, -34] [26, 52] [-117, 58] [-60, -95] [-125, -35] [-60, 68]
[7, 54] [66, 78] [68, -61] [66, -83] [93, -123] [-124, -47] [15, 106] [2, -72]
If you want a concrete Matrix you can do collect(S), but depending on your application you may not need to.
Thank you for all your answers. I tried to benchmark the solutions (code below).
The results were for a 500 x 500 x 2 3D Array.
Using StaticArray, by @sdanisch : 4.280 ÎĽs (1 allocation: 5.06 KiB)
Using TensorCast, by @rafael.guerra : 515.948 ns (6 allocations: 288 bytes)
Using StaticArrays and StructArrays, by @piever : 403.785 ns (8 allocations: 368 bytes)
It seems using TensorCast as suggested by @rafael.guerra gives the best.
Code used:
using BenchmarkTools
# Using StaticArray, by @sdanisch
using StaticArrays
function method1(X)
ax = axes(X)
map(CartesianIndices((ax[1], ax[2]))) do i
x, y = Tuple(i)
SVector(ntuple(z -> X[x, y, z], 2))
end
end
# Using TensorCast, by @rafael.guerra
using TensorCast
function method2(X)
@cast out[i, j]{k} := X[i, j, k]
end
# Using StaticArrays and StructArrays, by @piever
using StaticArrays, StructArrays
function method3(X::AbstractArray{T, N}) where {T, N}
return StructArray{SVector{2, T}}(X, dims=N)
end
### Benchmarking
X = rand(Int8, 50, 50, 2);
@btime X1 = method1($X);
@btime X2 = method2($X);
@btime X3 = method3($X);
Note that if you go for the TensorCast solution (which IIUC under the hood uses reinterpret to reinterpret a given block of memory as a matrix of SVectors) you should try to have your data in the 2 x N x K rather than N x K x 2 format. Then you can just do reinterpret(reshape, SVector{2, Int8}, X) to get your matrix, as each SVector is stored contiguously in the array as X[:, j, k].
The N x K x 2 format is a “struct of arrays” format, where the first component of all the arrays X[:, :, 1] is stored contiguously, and so is the second component X[:, :, 2].
To see what I mean in practice, experiment benchmarking functions that use the resulting arrays. For example @btime sum($X2) (where X2 was computed with TensorCast) should be substantially slower than @btime sum($X1) or @btime sum($X3) (which hopefully should be very similar).
This is correct. It applies this to something like a PermutedDimsArray when given the N x K x 2 format, I’m somewhat surprised this works in fact, and would not expect it to be particularly fast. The simple re-interpretation is things like @cast out3[j,k]{i} := vol3[i,j,k], making SVectors from the first dimension.
I 'm not sure it knows about reinterpret(reshape, ... (which was new in Julia 1.6), so it inserts more reshapes than ideal:
julia> @cast out2[i,j]{k} := vol[i,j,k]; summary(out2)
"1000Ă—1000 reshape(reinterpret(StaticArrays.SVector{2, Float64}, reshape(transmute(::Array{Float64, 3}, (3, 1, 2)), 2000000)), 1000, 1000) with eltype StaticArrays.SVector{2, Float64}"