SMatrix{2, 2, Float64} is not a concrete type?

I’m writing some program which for the sake of aesthetics manipulates data as SMatrix{2,2, Float64}. However, I noticed that, having a collection of such SMatrices, say in variable Ms, the command sum(Ms) allocates memory. Insdeed,

using StaticArrays
Ms = Vector{SMatrix{2, 2, Float64}}();
for i in 1:1000
       push!(Ms, SMatrix{2, 2, Float64}(rand(2,2)))
end
@time sum(Ms)
# 0.000053 seconds (999 allocations: 46.828 KiB)
isconcretetype(SMatrix{2,2,Float64})
#  false

Indeed, calling @code_warntype sum(Ms) produces

MethodInstance for sum(::Vector{SMatrix{2, 2, Float64}})
  from sum(a::AbstractArray; dims, kw...) @ Base reducedim.jl:994
Arguments
  #self#::Core.Const(sum)
  a::Vector{SMatrix{2, 2, Float64}}
Body::Any
1 ─      nothing
│   %2 = Base.:(:)::Core.Const(Colon())
│   %3 = Core.NamedTuple()::Core.Const(NamedTuple())
│   %4 = Base.pairs(%3)::Core.Const(Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}())
│   %5 = Base.:(var"#sum#807")(%2, %4, #self#, a)::Any
└──      return %5

showing an errant Any. Surely this is not the intended behavior of StaticArrays? Is there a workaround?

1 Like

This gets me all the time, too! There’s a fourth type parameter that you are missing. You need SMatrix{2, 2, Float64, 4}. That final parameter is the total number of elements in the array.

julia> isconcretetype(SMatrix{2, 2, Float64, 4})
true
6 Likes

The reason this last parameter is required, as far as I have understood, is that the type system cannot perform arithmetic, and thus cannot figure out that 2*2 = 4. For the compiler to know the length of the backing tuple that is required to store all the data, you thus have to provide the 4 as well.

9 Likes

Basically nothing but subtyping can be done to type parameters because methods are so malleable. Imagine you have twice(x::Int) = 2x and somehow could do Rectangle{L, W=twice(L)} so that Rectangle{L} is concrete. But then you do twice(x::Int) = 3x so Rectangle{L} means something different and the existing instances become invalid. Complete type chaos. Just because Rectangle{1, 3} was impossible before doesn’t mean it won’t always be, so it’s good to keep all the parameters even if the type constructors’ bodies make some of them redundant at the moment.

Use something like

Ms = Vector{typeof(zero(SMatrix{2,2,Float64}))}()

to calculate the accidental type parameters.

1 Like

I’ve opened an issue for this with a suggested interface for turning something like SMatrix{2, 2, Float64} into SMatrix{2, 2, Float64, 4}: Add a function to get the computed field types from an incomplete `StaticArray` type · Issue #1206 · JuliaArrays/StaticArrays.jl · GitHub

2 Likes

Interesting. Is this something that could and should, and not too late(?), have been handled by a macro?

Maybe that’s exactly the reason from the docs:

A convenience macro @SMatrix [1 2; 3 4] is provided

Preceding:

Statically sized N×M matrices are provided by SMatrix{N,M,T,L}.

Here L is the length of the matrix, such that N × M = L. However,
convenience constructors are provided, so that L, T and even M are
unnecessary. At minimum, you can type SMatrix{2}(1,2,3,4) to create a 2×2
matrix (the total number of elements must divide evenly into N).

So this “convenience constructor” is for inconvenience? :slight_smile: I guess it’s to help to not have to know too much, and sometimes parts of code aren’t the bottleneck. But I’m not sure then you would want to be using SMatrix… Is this maybe better when L is rather large (but not too large then just go with regular Matrix?)? Should there be a warning or error when L is low. That seems like a breaking change… but might be ok.

Those are convenience constructors for instances. With a couple parameters and the inputs, it can work out and apply the remaining parameters. But the problem in the original post was not about constructing an instance, but was about specifying a container for holding instances of some type. When specifying a type separate from an instance (e.g., in struct fields or container parameters), it’s necessary to specify it fully. In method signatures for dispatch and object constructors that can successfully infer the remaining parameters, it is not necessary to specify types fully.

I try to use map or comprehensions (wherever possible) when constructing containers. They do a good job of correctly and automatically determining the proper container type. Doing these sorts of things by hand is pretty tedious and error-prone.

Also, note that

push!(Ms, SMatrix{2, 2, Float64}(rand(2,2)))

is allocating and filling a 2x2 Matrix{Float64} first, then using those values to build a SMatrix before discarding the Matrix. You should try @SArray randn(2,2) or randn(SMatrix{2,2}) instead, which will not create the temporary array.

2 Likes

Thank you everyone for the quick responses! @danielmatz that indeed solved my allocation issue.

@baggiponte @Benny I indeed came across this in a different setting, when I wanted to define a struct MyBuffer{N} that holds two SVectors, one of length N and one of length 6*N, but to no avail. I understand how the this is circumvented by custom constructors that enforce such invariants - are there any other standard practices?

More broadly, I’m terribly confused my the (sometimes, partial) syntax of parametric type specification when invoking constructors. By now I understand that sometimes they can be omitted if types are inferable from the constructor arguments, but then, which are skippable? Is the situation different between inner and “external” constructors? It seems to me the documentation on writing (sanitized, idiomatic) constructors is very thin.