Julep: Efficient Hierarchical Mutable Data

Come to think of it, sorry that I didn’t follow everything and maybe it’s been discussed, but

Couldn’t that be made to work? MArray uses a tuple internally to get the flat memory layout, but then uses unsafe_store! to guarantee that it’s modified efficiently. We can do the same for a struct. Something like

struct Elephant
     height::Int
end

mutable struct Forest::Mutable{(:elephant,)}
    elephant::Elephant
    name::String
end

f = Forest(Elephant(1), "Boya")
f.elephant.height = 10   # works thanks to getproperty returning a view

using a method like

@generated Base.getproperty(m::Mutable{T}, field) = 
     # check if field is in T, if so return a view whose `setproperty!` will use unsafe_store! to change the field

We’d still need complex views, but maybe no macro… It’s not so different from the macro proposal, but perhaps the type is more julian.

EDIT: fixed that Forest must be a mutable, of course.

Afaiu MArray doesn’t really work for pointer types, because of the missing intrinsic interior_pointerset to apply the right write barrier (meaning – if you use MArray with e.g. strings, you can get a nice crash / memory corruption / UAF security vuln).

As @foobar_lv2 I also think that this cannot work, but wanted to provide the link to the discussion as I think this is the approach I linked to in the section Earlier Attempts, where @andyferris had it implemented like this and then there was agreement, that it cannot work correctly in all cases (for non-isbits values) and he had to remove it again.

1 Like

Strictly speaking you wouldn’t when (implicitly) using setindex!, as this case nowadays throws an error. You can test it with

using StaticArrays
julia> MVector{1, String}(undef)[1] = "42"
ERROR: setindex!() with non-isbitstype eltype is not supported by StaticArrays. Consider using SizedArray.

However, you can probably get all of what you mentioned when using unsafe_store!. This is probably what you meant, I just wanted to highlight, that MArray is safe as long as you do not use any unsafe function.

1 Like

That’s a really good idea and can probably be generalized. We should go through the list of the shortcomings and see what we can improve. But let’s focus on the pointer_from_objref in this post.

It does work, but the first symbol of each substruct (the __a here) needs to be entered manually, which should be integrated into the macro:

includet("inline_struct.jl") # containing @cstjean's code above
abstract type AbstractPart end
mutable struct Part <: AbstractPart
    a::Float32
end
@with_inline_types mutable struct Whole2 
    b::Int32
    part1::@inline(Part)
    part2::@inline(Part) 
end

Base.fieldoffset(x::DataType, sym::Symbol) = fieldoffset(x, findfirst(sym |> ==, x |> fieldnames))

Base.pointer_from_objref(pv::PartView{S, W}) where {S, W} =
    (getfield(pv, :obj) |> pointer_from_objref) + fieldoffset(W, Symbol(S, "__a"))

julia> const w2 = Whole2(Int32(42), Part(42.0f0), Part(43.0f0));

julia> (w2, w2.part1, w2.part2) .|> pointer_from_objref
(Ptr{Nothing} @0x00007f8bb3d802b0, Ptr{Nothing} @0x00007f8bb3d802b4, Ptr{Nothing} @0x00007f8bb3d802b8)

I wouldn’t start a package like this by myself, but if you want to start from the above code (or some other implementation), I’m down to co-maintain. Alternatively, I could put it inside of QuickTypes.jl, where it’s somewhat thematically relevant.

While QuickTypes.jl indeed is related, I assume that there could be a lot of users using only the existing QuickTypes.jl functionality or the struct inlining discussed here. Therefore, a separate package looks cleaner to me.

Thanks for the offer to co-maintain. Let’s see how far we get here and how much I can get an understanding of your macro magic. This might really make sense.

1 Like

This even seems to work for inline arrays. We’ll need a macro implementation and we’ll need to see how it scales, but first results are very promising:

includet("inline_struct.jl")
abstract type AbstractInlineArray end
mutable struct InlineArray <: AbstractInlineArray
    a__1::Int8; a__2::Int8; a__3::Int8; a__4::Int8
    a__5::Int8; a__6::Int8; a__7::Int8; a__8::Int8
end
# Base.getindex(x::AbstractInlineArray, i) = getfield(x, Symbol("a__$i")) # ≈ 110 ns
function Base.getindex(a::AbstractInlineArray, i) # ≈ 2 ns
    i == 1 && return a.a__1
    i == 2 && return a.a__2
    i == 3 && return a.a__3
    i == 4 && return a.a__4
    i == 5 && return a.a__5
    i == 6 && return a.a__6
    i == 7 && return a.a__7
    i == 8 && return a.a__8
    BoundsError(a, i) |> throw
end
function Base.setindex!(a::AbstractInlineArray, val, i) # ≈ 2 ns
    i == 1 && return a.a__1 = val
    i == 2 && return a.a__2 = val
    i == 3 && return a.a__3 = val
    i == 4 && return a.a__4 = val
    i == 5 && return a.a__5 = val
    i == 6 && return a.a__6 = val
    i == 7 && return a.a__7 = val
    i == 8 && return a.a__8 = val
    BoundsError(a, i) |> throw
end
@with_inline_types mutable struct Whole3 
    b::Int32
    array::@inline(InlineArray)
end

julia> const w3 = Whole3(Int32(42), InlineArray(11, 12, 13, 14, 15, 16, 17, 18))
Whole3(42, 11, 12, 13, 14, 15, 16, 17, 18)

julia> w3.array[2] = Int8(22)
22

julia> w3
Whole3(42, 11, 22, 13, 14, 15, 16, 17, 18)
1 Like

I think I lumped too much here. Basically, we seem to have two approaches which me either might choose from or allow both. The semantics are fine if Julia can proof that …

  • … only one Part object gets created and returned. Then the new call of Part should effectively be modified to reuse memory from Whole.
  • … or the returned Part object did not escape. Then the Whole constructor can copy p into w after p’s creation when control flow returns to the Whole constructor without violating object identity (too much). Strictly speaking there could still be side effects, like conditional code execution based on p's memory address in the constructor of Part, but they seem exotic and can be addressed in the documentation.

PR here: `size` field: allow `Integer` values of singleton type as elements by nsajko · Pull Request #107 · JuliaArrays/FixedSizeArrays.jl · GitHub

1 Like