How to make a struct that contains a reference to other memory?

I have a vector of nodes, something like

struct Node
    x::Float64    
    # more fields
end
nodes = Vector{Node}(undef, bignumber)

And I have a vector of Elements that each contain some nodes

struct Element
    numNodes::UInt8
    tag::Int
    nodes::Vector{Node}
    # more fields
end
elems = [Element(n, i, nodes[inds]) for inds in indices]

This is terrible for size reasons, so my first thought is to use Ref since the manual makes it seem like the Julia version of pointers.

struct Element
    numNodes::UInt8
    tag::Int
    nodes::Vector{Ref{Node}}
    # more fields
end
elems = [Element(n, i, Ref.(nodes[inds])) for inds in indices]

But this doesn’t work, the only thing that seems to work is using SubArrays/Views

struct Element
    numNodes::UInt8
    tag::Int
    nodes::SubArray
    # more fields
end
elems = [Element(4,i,@view nodes[inds] #= etc =#) for inds in indices]

My question is then two fold:
Is this the correct way of doing this or is there a better more performant way to do and think about this in Julia?
How do you make a struct that contains a reference to another bit of memory? Since using a view would seem silly for something that isn’t an array.

P.S. Is a Ref not actually the Julia version of a Pointer?

Hi there!
Can you give a complete MWE, and explain why the solutions you have “don’t work” or are “terrible”? In particular, it seems indices is a vector of vectors?

2 Likes

MWE of Ref not working

julia> struct Foo
           y::Float64
       end

julia> struct Bar
           l::Int
           o::Vector{Ref{Foo}}
       end

julia> foos = [Foo(i) for i in 1:4]
4-element Vector{Foo}:
 Foo(1.0)
 Foo(2.0)
 Foo(3.0)
 Foo(4.0)

julia> bar = Bar(2, Ref.(foos[1:2]))
Bar(2, Ref{Foo}[Base.RefValue{Foo}(Foo(1.0)), Base.RefValue{Foo}(Foo(2.0))])

julia> foos[1] = Foo(5)
Foo(5.0)

julia> bar
Bar(2, Ref{Foo}[Base.RefValue{Foo}(Foo(1.0)), Base.RefValue{Foo}(Foo(2.0))])
# Foo(1.0) has not changed

The reason

struct Element
    numNodes::UInt8
    tag::Int
    nodes::Vector{Node}
    # more fields
end

is terrible is because in the case that you have many Elements referencing the same nodes you are copying the data of those nodes, thus this is a waste of memory.

Each Element could reference an arbitrary subset of nodes which is why indices is a vector of the indices of the nodes that each Element has.

My goal is to minimize memory usage since I could be making hundreds of thousands of Nodes, each of which must belong to at least one Element.

From reading the manual it seems like one should be able to use Ref to reference another piece of memory like pointers do in C, am I mistaken?

I apologize for not giving a more thorough explanation.

1 Like

foo[1:2] makes a copy of the elements.
What you want to use here is bar = Bar(2, [ Ref(foos, i) for i in 1:2 ]). See the doc string of Ref.

Also note that

julia> isabstracttype(Ref{Foo})
true

which will make iterating over this vector slow.
You should instead use Vector{Base.RefValue{Foo}}.


Yes, they can be used that way.

But if you are dealing with arrays, I would recommend you to work with view and SubArrays instead.

E.g.

julia> struct Bar{V<:AbstractVector{<:Foo}}
           l::Int
           o::V
       end

julia> bar = Bar(2, view(foos, 1:2))
4 Likes

You could make Node be a mutable struct. This way, putting some of them in an array doesn’t copy the content around, just a pointer.

Another solution is to use views. You already suggested this in your OP but didn’t really say why that’s a bad idea. Seems reasonable to me.

2 Likes

Using views does make sense, though the context in how I’ve seen views used is very different from how I’ve seen pointers used on other languages (i.e. a way to avoid copying, rather than a data type in and of themselves).
And I was wondering if there was a convention I was missing.

My main point of confusion was how different the syntax is when pointing to an array vs not an array (as shown by @fatteneder ). And I didn’t know that broadcasting made copies of the arguments.

Not only broadcasting makes a copy.
In the example above already slicing the array with foos[1:2] makes a copy. And then calling Ref.(foos[1:2]) makes a second copy.

1 Like

Just one more question:
Why V<:AbstractVector{<:Foo} and not V<:SubArray?
Do these tell the compiler substantiality different things or is this a human readability choice?

Its more general.
If you were to restrict to V<:SubArray you couldn’t call

julia> bar = Bar(2, foo)
1 Like

There is almost never a performance benefit to tightening constraints on type parameters (unless maybe the constraint is a concrete type, but then it should not be a parameter). They are mostly just there for for their ability to add convenient guardrails against stuff going too crazy. They should mostly be unconstrained or constrained to broad, abstract types.

In your case, maybe there’s some world where a NTuple{3,Foo} would be reasonable in that parameter. Though this is not <:AbstractVector{Foo}, it meets most of the interface for it and would probably work for most things. There’s no cost to leaving this as a hypothetical possibility, so it’s nice to have it.

At runtime, you will have a Bar{SubArray{<complete type parameterization to make a concrete type>)}}. This is what the compiler will actually specialize for. Since it specializes at that level, there isn’t real benefit to a tighter a-priori constraint.

1 Like

That brings up another thing I tried for the same project as this.

Is there a way to make something like

julia> struct Node
           dim::UInt8
           tag::UInt
           pos::NTuple{dim, Float64}
       end
ERROR: UndefVarError: `dim` not defined

Where the size of the NTuple is a parameter, like Node{Dim}? Or must one make many structs like

Node1D
Node2D
Node3D
...

And this would be to make the struct only use concrete types.

Yes. EDIT: I wasn’t reading closely so originally left dim as a field.

struct Node{dim}
  tag::UInt
  pos::NTuple{dim, Float64}
end
julia> fieldtype(Node{0}, :pos)
Tuple{}

julia> fieldtype(Node{1}, :pos)
Tuple{Float64}

julia> fieldtype(Node{2}, :pos)
Tuple{Float64, Float64}

julia> fieldtype(Node{3}, :pos)
Tuple{Float64, Float64, Float64}
1 Like

But drop dim as a field in the struct. It should just be a type parameter. If I’m not mistaken, the dim field is now unconnected with the parameter, and also wholly redundant.

julia> x = Node{3}(2, 10, (1.0, 2.1, 3.2));

julia> x.dim
0x02
2 Likes