Home > OS >  Julia: Abstract types vs Union of types
Julia: Abstract types vs Union of types

Time:12-09

I am quite new to Julia, and I still have some doubts on which style is better when trying to do certain things... For instance, I have lots of doubts on the performance or style differences of using Abstract types vs defining Unions.

An example: Let's imagine we want to implement several types of units (Mob, Knight, ...) which should share most (if not all) of their attributes and most (if not all) of their methods. I see two options to provide structure: First, one could declare an abstract type AbstractUnit from which the other types derive from, and then create methods for the abstract type. It would look something like this:

abstract type AbstractUnit end

mutable struct Knight <: AbstractUnit
    id     :: Int
    [...]
end

mutable struct Peasant <: AbstractUnit
    id     :: Int
    [...]
end

id(u::T)      where T <: AbstractUnit = u.id
[...]

Alternatively, one could define a union of types and create methods for the union. It would look something like this:

mutable struct Knight
    id     :: Int
    [...]
end

mutable struct Peasant
    id     :: Int
    [...]
end

const Unit = Union{Knight,Peasant}

id(u::Unit) = u.id
[...]

I understand some of the conceptual differences between these two approaches, and think the first one is more expandable. However, I have a lot of doubts in terms of performance. For instance, how bad would it be creating arrays of the AbstractUnit vs arrays of the union type in terms of memory allocation at runtime?

Thanks!

CodePudding user response:

Absolutely use AbstractUnit instead of Unit in your methods' argument type annotations. Unions are abstract types too, actually, but as you pointed out, you can't add new types to it. In either case, the method is compiled to a specialization for each concrete type like Knight or Peasant, so your methods' performance won't be different.

As for Arrays' element types, there is the isbits Union optimization, but as the name suggests it only works if all the types in your Union are isbitstypes (no pointers, immutable). Your structs are mutable so that's already not applicable. See, memory access is faster when instances are directly stored in Arrays; the element type parameter (T in Vector{T}) must be a concrete isbitstype to allow this. When the element type parameter is abstract, generally Arrays only directly store pointers to the actual instances because multiple concrete types can have unknown and varying memory size. If the abstract type is an isbits Union though, instances could be stored directly in the Array: enough memory is allocated per element to contain the largest concrete type in the Union as well as a tag byte for each element specifying the type. A byte only has 256 values, so presumably this only works for Unions of fewer types.

Another possible optimization by using a Vector{Unit} over Vector{AbstractUnit} is Union-splitting. I'm really not going to be able to make an example method that explains it better than the linked blog, so I'll just give the short version. Normally, when Julia's compiler fails to infer the concrete type of a variable in your method (a.k.a. type instability), inner method calls involving the variable must do type checks and dispatch (selecting the specialization) at run-time, which can cost significant time. However, if Julia's compiler can infer the variable to be a Union of a few types (in practice, at most 4), conditional branches for each type can be used to eliminate most type checks and do dispatch at compile-time. Vector{AbstractUnit} can contain any number of types <: AbstractUnit, so the compiler cannot use Union-splitting. Vector{Unit} however lets the compiler know the elements must be either Knight or Peasant, which allows Union-splitting.

P.S. This is often a source of confusion for beginners, but while Unit is an abstract type, Vector{Unit} is a concrete type, just with an abstract type parameter. After all, it unambiguously describes an Array directly containing pointers to Knight or Peasant instances.

  • Related