Less verbose way of avoiding type instability?

I have a simple wrapper type like so:

struct Wrapper{T <: Real}
	x::AbstractMatrix{T}
	y::AbstractVector{T}
end

and a simple function operating on Wrapper objects:

f(w) = w.x * w.y

The problem is that f(w::Wrapper) seems to be type unstable - red ::Any in @code_warntype, and in my actual use case (more fields in the wrapper type, f is not so simple), profiling suggests a fair amount of time spent on type inference.

If the wrapper is parameterized more explicitly, then f becomes stable:

struct WrapperVerbose{T1 <: AbstractMatrix{<:Real}, T2 <: AbstractVector{<:Real}}
	x::T1
	y::T2
end

But that’s quite wordy and doesn’t scale nicely when there are more fields.

What’s the preferred way to have type stable functions operating on wrapper types? In performance-sensitive code is it bad to have abstract type annotations on fields? (I did that because sometimes x is small and dense, and sometimes it is large and very sparse and using a sparse type helps a lot.)

In general, that’s the way to write a wrapper, I don’t think you can escape it. Note that your first definition enforces that the element types of both objects are the same, but the second definition doesn’t.

I wrote a macro to help with parametric definitions:

julia> using QuickTypes

# _fp stands for fully_parametric
julia> @qstruct_fp WrapperVerbose(x::AbstractMatrix{<:Real}, 
                                  y::AbstractVector{<:Real})

It’s equivalent to your second definition, and should be type-stable. Ideally it would support @qstruct_fp WrapperVerbose{T}(x::AbstractMatrix{T}, ...) too.

Yes.

2 Likes

Yes, indeed, the cause of your troubles is that for composite types, it is very bad (from a performance viewpoint) to have it’s members specified as abstract types. This is because the compiler cannot then make any assumptions about the storage of fields in the object, and thus needs to run checks for every access of the data. A longer explanation is in the documentation: https://docs.julialang.org/en/stable/manual/performance-tips/#Avoid-fields-with-abstract-type-1

It might be nice if

struct Wrapper
	x::AbstractMatrix{<:Real}
	y::AbstractVector{<:Real}
end

was sugar for

struct Wrapper{T <: Real}
	x::AbstractMatrix{T}
	y::AbstractVector{T}
end

That would be confusing and prone to performance bugs in my opinion since it becomes difficult to see what the actual parameters are for a struct. Performance bugs because of acidentally leaving out the typeval when wrapping that struct.

True, that could be problematic. OTOH QuickTypes provides a nice compact syntax for this, so that is probably the better alternative.