Type instability with static matrices when full size not given?

I ran across the following, very wierd situation: if I don’t specify the 4th parameter in a SMatrix (which is supposed to be computed as the product of the sizes, then the code slows down considerably. MWE:

using BenchmarkTools
using StaticArrays

Good = SMatrix{1,1,Int,1}
Bad = SMatrix{1,1,Int}

struct Container{T} x::T end
fetch(c::Container{T}) where T = c.x

let
    list = [Good([i]) for i=1:1000]
    global goodprods(x) = [fetch(x)*y for y∈list]
end

let
    list = [Bad([i]) for i=1:1000]
    global badprods(x) = [fetch(x)*y for y∈list]
end

Then
@benchmark goodprods(m) setup = (m = Container{Good}(Good([1]))) gives 1 allocation, median time 1.236 μs, while
@benchmark badprods(m) setup = (m = Container{Bad}(Bad([1]))) gives 2001 allocations, median time 31.831 μs.

Your Container has an non-concretely typed field, since you force it to be non-concretely typed with the incompletely typed Bad. Inference thus can’t assume more about the item, even though the actual object has all type parameters set (as all objects at runtime do). You can check this by using functions that return functions instead of your lets - only those passed in with Container{Bad} are type unstable, even if they access “Bad” lists.

julia> typeof(m_good)             
Container{SMatrix{1, 1, Int64, 1}}
                                  
julia> fieldtypes(Container{Good})
(SMatrix{1, 1, Int64, 1},)        
                                  
julia> typeof(m_bad)              
Container{SMatrix{1, 1, Int64}}   
                                  
julia> fieldtypes(Container{Bad}) 
(SMatrix{1, 1, Int64},)           
Click for expanded @code_warntype info.
julia> function good_gen()                                                                                                        
           list = [ Good([i]) for i in 1:1000 ]                                                                                   
           x -> [ fetch(x)*y for y ∈ list ]                                                                                       
       end                                                                                                                        
good_gen (generic function with 2 methods)                                                                                        
                                                                                                                                  
julia> function bad_gen()                                                                                                         
           list = [ Bad([i]) for i in 1:1000 ]                                                                                    
           x -> [ fetch(x)*y for y ∈ list ]                                                                                       
       end                                                                                                                        
bad_gen (generic function with 2 methods)                                                                                         
                                                                                                                                  
julia> good_prod = good_gen()                                                                                                     
#34 (generic function with 1 method)                                                                                              
                                                                                                                                  
julia> bad_prod = bad_gen()                                                                                                       
#40 (generic function with 1 method)                                                                                              
                                                                                                                                  
julia> m_bad = Container{Bad}(Bad([1]))                                                                                           
Container{SMatrix{1, 1, Int64}}([1])                                                                                              
                                                                                                                                  
julia> m_good = Container{Good}(Good([1]))                                                                                        
Container{SMatrix{1, 1, Int64, 1}}([1])                                                                                           
                                                                                                                                  
julia> @code_warntype good_prod(m_good)                                                                                           
MethodInstance for (::var"#34#37"{Vector{SMatrix{1, 1, Int64, 1}}})(::Container{SMatrix{1, 1, Int64, 1}})                         
  from (::var"#34#37")(x) in Main at REPL[35]:3                                                                                   
Arguments                                                                                                                         
  #self#::var"#34#37"{Vector{SMatrix{1, 1, Int64, 1}}}                                                                            
  x::Container{SMatrix{1, 1, Int64, 1}}                                                                                           
Locals                                                                                                                            
  #35::var"#35#38"{Container{SMatrix{1, 1, Int64, 1}}}                                                                            
Body::Vector{SMatrix{1, 1, Int64, 1}}                                                                                             
1 ─ %1 = Main.:(var"#35#38")::Core.Const(var"#35#38")                                                                             
│   %2 = Core.typeof(x)::Core.Const(Container{SMatrix{1, 1, Int64, 1}})                                                           
│   %3 = Core.apply_type(%1, %2)::Core.Const(var"#35#38"{Container{SMatrix{1, 1, Int64, 1}}})                                     
│        (#35 = %new(%3, x))                                                                                                      
│   %5 = #35::var"#35#38"{Container{SMatrix{1, 1, Int64, 1}}}                                                                     
│   %6 = Core.getfield(#self#, :list)::Vector{SMatrix{1, 1, Int64, 1}}                                                            
│   %7 = Base.Generator(%5, %6)::Base.Generator{Vector{SMatrix{1, 1, Int64, 1}}, var"#35#38"{Container{SMatrix{1, 1, Int64, 1}}}} 
│   %8 = Base.collect(%7)::Vector{SMatrix{1, 1, Int64, 1}}                                                                        
└──      return %8                                                                                                                
                                                                                                                                  
                                                                                                                                  
julia> @code_warntype good_prod(m_bad)                                                                                            
MethodInstance for (::var"#34#37"{Vector{SMatrix{1, 1, Int64, 1}}})(::Container{SMatrix{1, 1, Int64}})                            
  from (::var"#34#37")(x) in Main at REPL[35]:3                                                                                   
Arguments                                                                                                                         
  #self#::var"#34#37"{Vector{SMatrix{1, 1, Int64, 1}}}                                                                            
  x::Container{SMatrix{1, 1, Int64}}                                                                                              
Locals                                                                                                                            
  #35::var"#35#38"{Container{SMatrix{1, 1, Int64}}}                                                                               
Body::Vector                                                                                                                      
1 ─ %1 = Main.:(var"#35#38")::Core.Const(var"#35#38")                                                                             
│   %2 = Core.typeof(x)::Core.Const(Container{SMatrix{1, 1, Int64}})                                                              
│   %3 = Core.apply_type(%1, %2)::Core.Const(var"#35#38"{Container{SMatrix{1, 1, Int64}}})                                        
│        (#35 = %new(%3, x))                                                                                                      
│   %5 = #35::var"#35#38"{Container{SMatrix{1, 1, Int64}}}                                                                        
│   %6 = Core.getfield(#self#, :list)::Vector{SMatrix{1, 1, Int64, 1}}                                                            
│   %7 = Base.Generator(%5, %6)::Base.Generator{Vector{SMatrix{1, 1, Int64, 1}}, var"#35#38"{Container{SMatrix{1, 1, Int64}}}}    
│   %8 = Base.collect(%7)::Vector                                                                                                 
└──      return %8                                                                                                                
                                                                                                                                  
                                                                                                                                  
julia> @code_warntype bad_prod(m_good)                                                                                            
MethodInstance for (::var"#40#43"{Vector{SMatrix{1, 1, Int64, 1}}})(::Container{SMatrix{1, 1, Int64, 1}})                         
  from (::var"#40#43")(x) in Main at REPL[36]:3                                                                                   
Arguments                                                                                                                         
  #self#::var"#40#43"{Vector{SMatrix{1, 1, Int64, 1}}}                                                                            
  x::Container{SMatrix{1, 1, Int64, 1}}                                                                                           
Locals                                                                                                                            
  #41::var"#41#44"{Container{SMatrix{1, 1, Int64, 1}}}                                                                            
Body::Vector{SMatrix{1, 1, Int64, 1}}                                                                                             
1 ─ %1 = Main.:(var"#41#44")::Core.Const(var"#41#44")                                                                             
│   %2 = Core.typeof(x)::Core.Const(Container{SMatrix{1, 1, Int64, 1}})                                                           
│   %3 = Core.apply_type(%1, %2)::Core.Const(var"#41#44"{Container{SMatrix{1, 1, Int64, 1}}})                                     
│        (#41 = %new(%3, x))                                                                                                      
│   %5 = #41::var"#41#44"{Container{SMatrix{1, 1, Int64, 1}}}                                                                     
│   %6 = Core.getfield(#self#, :list)::Vector{SMatrix{1, 1, Int64, 1}}                                                            
│   %7 = Base.Generator(%5, %6)::Base.Generator{Vector{SMatrix{1, 1, Int64, 1}}, var"#41#44"{Container{SMatrix{1, 1, Int64, 1}}}} 
│   %8 = Base.collect(%7)::Vector{SMatrix{1, 1, Int64, 1}}                                                                        
└──      return %8                                                                                                                
                                                                                                                                  
                                                                                                                                  
julia> @code_warntype bad_prod(m_bad)                                                                                             
MethodInstance for (::var"#40#43"{Vector{SMatrix{1, 1, Int64, 1}}})(::Container{SMatrix{1, 1, Int64}})                            
  from (::var"#40#43")(x) in Main at REPL[36]:3                                                                                   
Arguments                                                                                                                         
  #self#::var"#40#43"{Vector{SMatrix{1, 1, Int64, 1}}}                                                                            
  x::Container{SMatrix{1, 1, Int64}}                                                                                              
Locals                                                                                                                            
  #41::var"#41#44"{Container{SMatrix{1, 1, Int64}}}                                                                               
Body::Vector                                                                                                                      
1 ─ %1 = Main.:(var"#41#44")::Core.Const(var"#41#44")                                                                             
│   %2 = Core.typeof(x)::Core.Const(Container{SMatrix{1, 1, Int64}})                                                              
│   %3 = Core.apply_type(%1, %2)::Core.Const(var"#41#44"{Container{SMatrix{1, 1, Int64}}})                                        
│        (#41 = %new(%3, x))                                                                                                      
│   %5 = #41::var"#41#44"{Container{SMatrix{1, 1, Int64}}}                                                                        
│   %6 = Core.getfield(#self#, :list)::Vector{SMatrix{1, 1, Int64, 1}}                                                            
│   %7 = Base.Generator(%5, %6)::Base.Generator{Vector{SMatrix{1, 1, Int64, 1}}, var"#41#44"{Container{SMatrix{1, 1, Int64}}}}    
│   %8 = Base.collect(%7)::Vector                                                                                                 
└──      return %8                                                                                                                

I think those kinds of incompletely typed type parameters should at least be colored differently in @code_warntype though, that would make the issue more apparent. Do you mind opening an issue about this?

Thanks! So the culprit is spotted in the very last line, %8 = Base.collect(%7)::Vector ? Indeed I had tried to use @code_warn but couldn’t make anything of its output.

I’m still troubled by the fact that StaticArrays documentation says that the last argument is computed automatically, while it seems that as soon as StaticArrays are used inside another structure then the last argument should by supplied explicitly, under penalty of huge slowdown. Do I understand this correctly? Is this a bug in StaticArrays, or just something that should be documented to warn poor souls like me in the future?

You’re misunderstanding things:

julia> list_bad = [Bad([i]) for i=1:1000];

julia> eltype(list_bad) == Good
true                                                               

The last parameter is calculated correctly, even for just Bad:

julia> Bad([1]) |> typeof                                           
SMatrix{1, 1, Int64, 1} (alias for SArray{Tuple{1, 1}, Int64, 2, 1})

It’s just that in Container{Bad}, you force the type of its only field to be non-concrete, even if the element saved there is completely specified:

julia> ex1 = Container{Bad}(Bad([1]))                               
Container{SMatrix{1, 1, Int64}}([1])                                
                                                                    
julia> fieldtypes(typeof(ex1))                                      
(SMatrix{1, 1, Int64},)                                             
                                                                    
julia> typeof(ex1.x)                                                
SMatrix{1, 1, Int64, 1} (alias for SArray{Tuple{1, 1}, Int64, 2, 1})

Since inference relies on the fieldtype to check which type to propagate, it fails and just gives you Vector. After all, at compile time it can’t know that there will be an element in there with a tighter specification than what you explicitly request with Container{Bad}. Additionally, inference is not aware of the special invariant imposed by the first two type parameters on the last one, so it can’t assume that it will be concretely typed anyway.

E.g. with Container{Bad} it’s the same as if you had written it like this:

struct BadContainer
   x::SMatrix{1,1,Int64} # non-concrete type.
end

All this is generally documented, in the form of “avoid abstract/non-concrete fields”.

I have opened an issue about the printing in @code_warntype.

Note that this doesn’t have anything to do with the semantics at play here - those are working as intended, as far as I can tell.