Ref vs zero-dimensional arrays?

What is the difference between a zero-dimensional array holding the value x, which you construct using fill(x), and a Ref(x)?

In both cases you would access the underlying data via [], which can be set or read.
And it seems that both store a pointer to the actual data.

julia> typeof(fill('a'))
Array{Char,0}

julia> typeof(Ref('a'))
Base.RefValue{Char}

Correct, but this does not mean that they are the same thing.

They don’t — the concept of pointers is not helpful in understanding composite types or containers in Julia.

One is an array — which means that it supports a bit more arithmetic and such like:

julia> fill(1) + fill(2)
3

Ref is about as barebones of a 0-dimensional container as you can get. But just like all 0-dimensional containers, it’ll work nicely with broadcast.

1 Like

I typically use them to have a simple (quick and dirty) modifiable field in a struct or for a pass-by-reference argument into a function. I presume there is no real performance difference between using one or the other in such a case?

Ref is an abstract type so can have performance overhead if stored in a struct.

julia> struct S
           s::Ref{Int}
       end

julia> b = S(1)
S(Base.RefValue{Int64}(1))

julia> f(b) = b.s[]
f (generic function with 1 method)

julia> @code_warntype f(b)
Variables
  #self#::Core.Compiler.Const(f, false)
  b::S

Body::Any
1 ─ %1 = Base.getproperty(b, :s)::Ref{Int64}
│   %2 = Base.getindex(%1)::Any
└──      return %2
1 Like

Array is always a pretty fat jl_array from the runtime, always heap allocated, etc.

In contrast, RefValue is almost the same as a mutable struct with a single field plus convenience access functions. This means that the optimizer can see through Ref, and often stack allocate it or refuse to allocate it alltogether.

An especially important idiom is to use unsafe_store! / unsafe_load on pointer_from_objref on refs in order to fiddle with bits / reinterpret between arbitrary bitstypes. The compiler is quite good at optimizing out that kind of stuff. This would not work with Array.

2 Likes

I guess one can use

struct S
    s::Base.RefValue{Int}
end
S(x::Int) = S(Ref(x))

instead. Then

julia> @code_warntype f(b)
Body::Int64
1 ─ %1 = (Base.getfield)(b, :s)::Base.RefValue{Int64}
│   %2 = (Base.getfield)(%1, :x)::Int64
└──      return %2

yep, that’s one level of pointer indirection less than Array. However, mutable struct S s::Int end is still one less level of pointer indirection (and one less allocation).

I think that Refwas hijacked incidentally for this purpose, possibly following this suggestion. Cf

On 1.2-pre, ?Ref still does not mention that it has anything to do with broadcasting.

In retrospect I think that this choice was unfortunate, as it conflates semantics. Also, I am not sure that many users are aware that Ref{T} is not a concrete type.

3 Likes

Just the other day a colleague asked me how to make something behave as a scalar under broadcasting. I showed him Ref and he was suprised that he couldn’t find anything about it in the docs.

Ref doesn’t really have anything to do with broadcasting. It’s just a convenient, short-named zero-dimensional container that doesn’t have the overhead of the Array header. As long as Ref is a zero-dimensional container, I don’t see any conflating of semantics. Now, that view isn’t entirely historically accurate, as Refs were indeed previously “hijacked” and special-cased to behave like 0-dimensional arrays only for the purposes of broadcast, but these days they really are 0-dimensional like and they just behave appropriately.

I was imagining that Ref would only be transiently visible to users until the &x syntax landed, but that got tied up and slogged down. I’d still love to see that, then the choice of how we implement scalars would be entirely internal. It’s indeed the &x syntax that could unites the ccall and broadcast use-cases.

You can also use (x,) or [x] as 1-tuples and 1-vectors also broadcast (almost) entirely like scalars. The tuple will of course be more efficient than the vector.

3 Likes

I meant that Ref is now the de facto solution for escaping broadcasting.

You are of course right, and indeed any 0-dimensional container would do, so in this sense Ref isn’t special, but in practice Ref has assumed this role.

It would be somewhat cleaner if we had something like Scalar proposed in the above PR, with the sole purpose of just escaping broadcasting, and no shared meaning with anything else. This a style question, but I think an important one.

2 Likes