Performance issues with constructors for arbitrary types

Hello all. I’ve been struggling now for a good while with a performance issue that seems it should be quite trivial to resolve, but so far, an elegant solution has eluded me.

So, I have an object t::Table{T} which lazily loads (from unstructured, untyped IO) the fields of an instance of T. I want to construct from this an instance of T. My thinking is that, if T is mutable, this should be a no-op.

I would like to do this with

(t::Table{T})() where {T} = T((getproperty(t, n) for n ∈ fieldnames(T))...)

however, this performs rather badly:

julia> @btime $t()
  14.923 μs (93 allocations: 5.48 KiB)

On the other hand, if I do

function (t::Table{T})() where {T}
    m = T()  # this is definitely wasting its time on defaults
    for s ∈ fieldnames(T)
        setfield!(m, s, getproperty(t, s))
    end
    m
end

I do somewhat better

julia> @btime $t()
  11.636 μs (69 allocations: 4.28 KiB)

which is a little surprising especially because I don’t even have an appropriate constructor for this (T() is definitely doing some unnecessary allocations).

I have often been confused about the performance consequences of splatting in the past. I have often been surprised with it not seeming to cost me anything, but in other cases like this, it’s seemingly disastrous.

So, what’s the best way to declare this constructor? Right now the best I could do would be to define a T(undef) that does as little as possible and then set the fields afterward.

In my experience splatting is fast when the compiler can determine the number and type of arguments, but when it can’t you take a significant hit. I strongly suspect the return type of getproperty is not inferred.

1 Like

Right, it’s not. Actually this is a somewhat separate issue I’ve been trying to deal with: I’d like to convince the compiler that getproperty should give types according to the fields of T, but the only way I can think of doing this so far is with some rather elaborate code generation that I’d like to avoid.

This still begs the question though, what’s the right way of doing this in this situation?

Do you know ahead of time the type of object you’re trying to load?

Yes, it would look something like deserialize(T, io), but so far I’ve mostly failed to get the compiler to take advantage of the fact that the schema is actually known. The functions which fetch the individual fields are not type stable, because there’s basically no way of asserting the types of each field (again, without some fairly elaborate code generation).

I mean, I suppose it’s obvious to me by now that the only thing I can really do is define a constructor that just calls new() and fill in the fields after, it’s just a pretty unsatisfying result, because there’s no other reason it should have such a constructor (and because of the nature of the problem, if I override the default constructors I’ll need some more elaborate macros).

1 Like