Clarification of Abstract Type and Specialized Code

In the performance tips, under Annotate values taken from untyped locations, it says:

Type annotation will not enhance (and can actually hinder) performance if the type is abstract, or constructed at run-time. This is because the compiler cannot use the annotation to specialize the subsequent code, and the type-check itself takes time.

When it says the abstract, does it mean that this code will hinder performance?

abstract type Character end
struct SpaceMarine <: Character end

svetlana :: Character = SpaceMarine()

Would it be better to write:

svetlana = SpaceMarine()

Suppose if I were to expand my types:

abstract type Character end

abstract type Infantry <: Character
abstract type Medic <: Character

struct SpaceMarine <: Infantry end
struct SARC <: Medic end 

Without type annotations svetlana could be a Medic without throwing an error:

svetlana = SARC()

What if I want to keep svetlana within the infantry subtype without hindering performance, what is the recommended way of doing it?

This just does a convert, in this case, nothing happens. Variables in Julia doesn’t need type annotation, they are what they.

julia> function f()
           a::Integer = 3
           return a
       end
f (generic function with 1 method)

julia> @code_warntype f()
MethodInstance for f()
  from f() in Main at REPL[10]:1
Arguments
  #self#::Core.Const(f)
Locals
  a::Int64
Body::Int64
1 ─ %1 = Base.convert(Main.Integer, 3)::Core.Const(3)
│        (a = Core.typeassert(%1, Main.Integer))
└──      return a::Core.Const(3)

In the hierarchy you’ve written, you can’t do that:

julia> convert(Infantry, svetlana)                                                       
ERROR: MethodError: Cannot `convert` an object of type SARC to an object of type Infantry
Closest candidates are:                                                                  
  convert(::Type{T}, ::T) where T at essentials.jl:218                                   
Stacktrace:                                                                              
 [1] top-level scope                                                                     
   @ REPL[6]:1                                                                           

Your hierarchy is this:

  • Character
    • Infantry
      • SpaceMarine
    • Medic
      • SARC

A SARC here can never be an Infantry, as SARC is only a subtype of Medic, which is a subtype of Character.


What is meant by

is that when you have some untyped source (like the Vector{Any} in the example you linked) and annotate an element you get from it with an abstract type like Character, you don’t help the compiler with figuring out which methods to choose down the road when passing that element around, because it still has to check the concrete runtime type for dispatch. In julia, all values have a concrete type which is used for dispatch. Abstract types are only used when defining methods, for restricting the types of arguments the subtypes of the annotated type - asserting an abstract type for a variable means inserting more type checks, which can hinder performance.

You see, the confusing part is, in the documentation for type annotations, it says differently.

To provide extra type information to the compiler, which can then improve performance in some cases

If I were to do:

svetlana :: Character = SpaceMarine()

Is it correct to say, I’m not hindering performance, but I’m not helping it either? In other words, it doesn’t do anything other than indicate that svetlana is a type of Character?

Ah yes, I can see how that may sound conflicting :thinking:

The documentation is thinking of situations where a variable would be assigned a type unstable result, forcing the variable to have the Union as its type as well. In that case, asserting a type (either concrete or abstract) can indeed improve performance, because that can eliminate the union. If the asserted type is part of the union, this helps the compiler.

If however the type is Any or the asserted type is not part of the union, the compiler will not be able to make use of the information (it sees conflicting information after all), having to insert a dynamic call to the conversion method instead of just a static type check at compile time. This is the situation I was thinking of.

In this specific case, yes, assuming SpaceMarine() is a type stable function. You’re not gaining anything when called functions are type stable, which we’re always striving for anyway. That’s why the manual says “can improve performance” (emphasis mine), because in the vast majority of cases it probably won’t, but it’s not impossible either.

I haven’t checked, but I’m fairly certain that because SpaceMarine is already a concrete type and you’re asserting an abstract super type of that, the compiler may just ignore your assertion and tag the variable SpaceMarine anyway (provided nothing else with a different subtype of Character gets assigned to that variable).

Ah! Much appreciated.

If you’re striving to write clean maintainable code and want to include the type annotations, provided SpaceMarine() is a type stable function, it would be okay, correct? Much like type hints in Python.

It doesn’t hurt, but it also won’t help further :man_shrugging:

I’ve come to the understanding that if I need type annotations in long functions (such that the possibly typed arguments to the functions are not enough), I should write smaller/more composable functions since julia is kinda good at deciding when to inline and when not to inline. You’re of course free to have a different custom though.

2 Likes