# Avoiding Vectors of Abstract Types

**URL:** <https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883>\
**Category:** Performance\
**Tags:** question, type-stability\
**Created:** [May 26, 2021, 7:20pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883 "2021-05-26T19:20:06Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![RobertGregg](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/robertgregg/32/22105_2.png) [@RobertGregg](https://discourse.julialang.org/u/RobertGregg)\
**Post date:** [May 26, 2021, 7:20pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/1 "2021-05-26T19:20:07Z")

</div>

I’ve come across a few instances (see [1](https://github.com/JuliaDynamics/Agents.jl/issues/445), [2](https://discourse.julialang.org/t/performance-drawback-with-subtyping/51939), [3](https://discourse.julialang.org/t/type-stability-collections-of-abstract-types/58525)) where it would be intuitive to create a vector of structs that all fall under the same abstract type (maybe this comes from an OO style of thinking?). Unfortunately, this leads to type instability which can cause a noticeable hit on performance. My question is, **what is good/general way to restructure code to avoid this problem?** I realize this is pretty problem dependent, but people generally seem to encounter this problem for a few reasons:

- You need a container to hold all the subtypes together
  - Often you’re indexing into this container to modify/add/delete subtypes

- You need to access a common field for one/some/all of the subtypes
- You need to apply some function to the collection of subtypes
- … (this is what I came up with off the top of my head)

My first thought is to maybe organize the subtypes into a “meta-struct”. Each field is a vector of the same concrete subtypes, but each instance is given an index. This solution would have a lot of overlap with [MultiScaleArrays.jl](https://github.com/SciML/MultiScaleArrays.jl), except the leaf node would be the struct (?). I would love input or references to possible solutions to this problem. Reference 3 to my previous post gives a decent MWE for this problem.

---

<div class="post-metadata">

**Author:** ![Henrique\_Becker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/henrique_becker/32/15443_2.png) [@Henrique\_Becker](https://discourse.julialang.org/u/Henrique_Becker)\
**Post date:** [May 26, 2021, 9:10pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/2 "2021-05-26T21:10:13Z")

</div>

One solution is to not use many different structs. Instead of creating a `Bow` and a `Sword` as subtypes of `Weapon`, create a concrete weapon that has a small `range` for the `Sword` and a long for the `Bow`, if necessary create a field `type` and put a constant `SWORD` or `BOW` there. This has the disavantages: (1) if you want external libraries to add new weapons `type`s the process will be clunky; (2) if the types are distinct because they need to have different representations then you may have a heavy and complex combined type.

---

<div class="post-metadata">

**Author:** ![RobertGregg](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/robertgregg/32/22105_2.png) [@RobertGregg](https://discourse.julialang.org/u/RobertGregg)\
**Post date:** [May 26, 2021, 9:38pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/3 "2021-05-26T21:38:11Z")

</div>

This is definitely a valid solution and something I’ve used in the past. Another disadvantage I’ve found to this approach is that is doesn’t scale well. Having 50 weapons all defined by one struct would become unruly.

---

<div class="post-metadata">

**Author:** ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)\
**Post date:** [May 26, 2021, 11:45pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/4 "2021-05-26T23:45:56Z")

</div>

> [@RobertGregg](#):
>
> Each field is a vector of the same concrete subtypes, but each instance is given an index

If that is possible this is by far the best approach, I think.

I don’t know if you already followed this thread, a lot has been discussed there, and that links to other related topics (one the things tried is that approach, which is the faster of all for the toy examples there). This last thread introduces new perspectives as well:

> [@Union splitting vs C++](https://discourse.julialang.org/t/union-splitting-vs-c/61772/1):
>
> This is a continuation from this thread: [Performance drawback with subtyping - #31 by paulmelis](https://discourse.julialang.org/t/performance-drawback-with-subtyping/51939/31) Here we have two codes, which compute someting simple (the sum of the values of a field of a type in an array of mixed types of objects). Typical case where it is hard to be type-stable and avoid run-time dispatch. With 2 different types in the list of types, union splitting kicks in in Julia and the code is fast. With more (in the present example 5 types), it does not. In the C++ code clearly it do…

---

<div class="post-metadata">

**Author:** ![Tamas\_Papp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tamas_papp/32/25949_2.png) [@Tamas\_Papp](https://discourse.julialang.org/u/Tamas_Papp)\
**Post date:** [May 27, 2021, 9:06am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/5 "2021-05-27T09:06:37Z")

</div>

> [@RobertGregg](#):
>
> **what is good/general way to restructure code to avoid this problem?** I realize this is pretty problem dependent

This really depends on your requirements, and is hard to answer without an MWE that reflects the dimension and complexity of your problem.

Generally, type instability and dynamic dispatch can be perfectly fine (and even beneficial, when it comes to compilation times) if used sparingly. Use [function barriers](https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions) and similar techniques to contain type instability as necessary.

---

<div class="post-metadata">

**Author:** ![filchristou](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/filchristou/32/26760_2.png) [@filchristou](https://discourse.julialang.org/u/filchristou)\
**Post date:** [January 26, 2022, 10:00pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/6 "2022-01-26T22:00:48Z")

</div>

I spent the last three hours just to realize that `Vector{AbstractType}` is even faster than `Vector{ConcreteParameteric{}}`. Figured I could share that.

In this case I am wondering: Would it hurt to just use `Union` of all subtypes ? I mean is there a point where, when `Union` type gets too long it starts hurting efficiency ?

If not what do you think of employing metaprogramming in order to define such vectors as:

```julia
@abstract2union ve = Vector{AbstractType}()

```

where `@abstract2union` uses `subtypes(AbstractType)` in order to recursively find all concrete structs and make a new `Union` type out of them.

so something like:

```julia
@macroexpand @abstract2union ve = Vector{AbstractType}()
ve = Vector{Union{filter(!isabstracttype, recursivesubtypes(AbstractType))...}}

```

in julian pseudocode.

---

<div class="post-metadata">

**Author:** ![Henrique\_Becker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/henrique_becker/32/15443_2.png) [@Henrique\_Becker](https://discourse.julialang.org/u/Henrique_Becker)\
**Post date:** [January 27, 2022, 2:26am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/7 "2022-01-27T02:26:49Z")

</div>

> [@filchristou](#):
>
> I spent the last three hours just to realize that `Vector{AbstractType}` is even faster than `Vector{ConcreteParameteric{}}` . Figured I could share that.

This is uncommon, do you have a MWE you can share?

---

<div class="post-metadata">

**Author:** ![filchristou](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/filchristou/32/26760_2.png) [@filchristou](https://discourse.julialang.org/u/filchristou)\
**Post date:** [January 27, 2022, 8:12am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/8 "2022-01-27T08:12:35Z")

</div>

Yes sure.  
I tested it for different Fruits of type `FruitAbstract`:

```julia
using BenchmarkTools
import Parameters: @with_kw

abstract type FruitAbstract end

struct Fruit{T <: FruitAbstract}
    fr::T
end

@with_kw struct Apple <: FruitAbstract
    name::String = "apple"
    id::Int = 1
end

@with_kw struct Banana <: FruitAbstract
    name::String = "banana"
    id::Int = 2
end

@with_kw struct Peach <: FruitAbstract
    name::String = "peach"
    id::Int = 3
end

const FruitsUnion = Union{Apple, Banana, Peach}
const FruitsStrUnion = Union{Fruit{Apple}, Fruit{Banana}, Fruit{Peach}}

@inline (fru::Fruit)() = getfield(fru, :fr)
@inline Base.getproperty(fru::Fruit, sym::Symbol) = getfield(fru(), sym)
@inline Base.show(io::IO, ::MIME"text/plain", fru::Fruit) = print(io, fru())
#@inline Base.convert(::Type{Fruit{T}}, fru::T) where {T <: FruitAbstract} = Fruit(fru)
#@inline Base.convert(::Type{Fruit}, fru::T) where {T <: FruitAbstract} = Fruit(fru)
#@inline Base.convert(::Type{FruitAbstract}, fru::T) where {T <: FruitAbstract} = Fruit(fru)
#@inline Base.convert(::Type{T}, fru::Fruit{T}) where {T <: FruitAbstract} = fru()

name(fru::Fruit) = name(getfield(fru, :fr))
name(f::FruitAbstract) = f.name

basketAb = Vector{FruitAbstract}()
[push!(basketAb, Apple()) for _ in 1:300_000];
[push!(basketAb, Banana()) for _ in 1:300_000];
[push!(basketAb, Peach()) for _ in 1:300_000];
sum([f.id for f in basketAb])

basketFr = Vector{Fruit}()
[push!(basketFr, Fruit(Apple())) for _ in 1:300_000];
[push!(basketFr, Fruit(Banana())) for _ in 1:300_000];
[push!(basketFr, Fruit(Peach())) for _ in 1:300_000];
sum([f.id for f in basketFr])

basketUn = Vector{FruitsUnion}()
[push!(basketUn, Apple()) for _ in 1:300_000];
[push!(basketUn, Banana()) for _ in 1:300_000];
[push!(basketUn, Peach()) for _ in 1:300_000];
sum([f.id for f in basketUn])

basketFrUn = Vector{FruitsStrUnion}()
[push!(basketFrUn, Fruit(Apple())) for _ in 1:300_000];
[push!(basketFrUn, Fruit(Banana())) for _ in 1:300_000];
[push!(basketFrUn, Fruit(Peach())) for _ in 1:300_000];
sum([f.id for f in basketFrUn])

basketFrAp = Vector{Fruit{Apple}}()
[push!(basketFrAp, Fruit(Apple())) for _ in 1:900_000];
sum([f.id for f in basketFrAp])

basketAp = Vector{Apple}()
[push!(basketAp, Apple()) for _ in 1:900_000];
sum([f.id for f in basketAp])

@btime sum(f.id for f in basketAb)
# 36.923 ms (899490 allocations: 13.73 MiB)
#1800000

@btime sum(f.id for f in basketFr)
# 67.196 ms (1799490 allocations: 41.19 MiB)
#1800000

@btime sum(f.id for f in basketUn)
# 4.263 ms (2 allocations: 32 bytes)
#1800000

@btime sum(f.id for f in basketFrUn)
# 4.054 ms (2 allocations: 32 bytes)
#1800000

@btime sum(f.id for f in basketFrAp)
# 877.861 μs (2 allocations: 32 bytes)
#900000

@btime sum(f.id for f in basketAp)
# 882.688 μs (2 allocations: 32 bytes)
#900000

```

The fastest results are when the Vector is well defined as `Vector{Apple}` or `Vector{Fruit{Apple}}` in the range of 900 μs

Immediate after comes the case when the Vector is defined as a Union `Vector{Union{Apple, Banana, Peach}}` or `Vector{Union{Fruit{Apple}, Fruit{Banana}, Fruit{Peach}}` in the range of 4 ms _(4.5x slower than before)_

Right after comes the case of AbstractType with `Vector{FruitAbstract}` at 37 ms _(9x slower than before)_

And the slowest is when I use concrete parametric type without signifying the parameter: `Vector{Fruit}` with a time of 67 ms _(1.9x slower than before)_

 ![image](https://global.discourse-cdn.com/julialang/original/3X/5/9/59db0e04cefc07a8624d73c79d83c067d454c2f1.png)

---

<div class="post-metadata">

**Author:** ![non-Jedi](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/non-jedi/32/3645_2.png) [@non-Jedi](https://discourse.julialang.org/u/non-Jedi)\
**Post date:** [January 27, 2022, 4:57pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/9 "2022-01-27T16:57:57Z")

</div>

> [@filchristou](#):
>
> In this case I am wondering: Would it hurt to just use `Union` of all subtypes ? I mean is there a point where, when `Union` type gets too long it starts hurting efficiency ?

If I remember correctly, the small union optimization that makes `Vector{Union{...}}` efficient has a limit of `typemax(UInt8)` types in the union, but [performance will degrade due to other compiler factors when going above 3-4 types](https://discourse.julialang.org/t/performance-tips-for-union-types/13928/5). Unless this has changed since 2018 which it very well might have.

---

<div class="post-metadata">

**Author:** ![Henrique\_Becker](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/henrique_becker/32/15443_2.png) [@Henrique\_Becker](https://discourse.julialang.org/u/Henrique_Becker)\
**Post date:** [January 28, 2022, 12:13am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/10 "2022-01-28T00:13:08Z")

</div>

Oh, I did not understood you meant a `ConcreteParametric` type **without any of its type parameters specified**. This is a little more expected (a `ConcreteParametric` type without its type parameters is not truly concrete), but its seems a large difference between simply abstract types and an `UnionAll` type. Only someone more familiar with the compiler internals than me would be able to answer why such large difference.

---

<div class="post-metadata">

**Author:** ![j-fu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/j-fu/32/11373_2.png) [@j-fu](https://discourse.julialang.org/u/j-fu)\
**Post date:** [February 7, 2022, 5:50pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/11 "2022-02-07T17:50:14Z")

</div>

It would indeed be interesting to learn what the performance limitations of built-in union splitting are nowadays and what could be possible future improvements. I just checked this pattern for a union of 20 types, there it seems to work.

It works (same performance as manual dispatch) for:

- vectors of instances of different types (as discussed in this thread), via `Vector{Union{T1,T2}}`
- for vectors of concrete types - one would use  
`Vector{Union{Type{T1},Type{T2}}}`
- for vectors of abstract types, same as for concrete ones
- for vectors of functions, also with multiple methods via `Vector{Union{typeof(f1),typeof(f2)}}`

I think these patterns reflect important use cases. If the limitations are indeed benign nowadays, this should go into the docs…

EDIT: [#37378](https://github.com/JuliaLang/julia/pull/37378) removed the union size limit for “single dispatch sites”. AFAIU this is the case here. The manual says: " “Small” is defined by the MAX\_UNION\_SPLITTING constant, which is currently set to 4." It seems that this is at least partially obsolete. Just wasn’t able to find the place where it is set to 4, during the search for this I found that PR.

Will try a PR to the docs when I find time.

---

<div class="post-metadata">

**Author:** ![j-fu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/j-fu/32/11373_2.png) [@j-fu](https://discourse.julialang.org/u/j-fu)\
**Post date:** [February 9, 2022, 6:10pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/12 "2022-02-09T18:10:58Z")

</div>

See some benchmarking [here](https://j-fu.github.io/marginalia/assets/unionize.html)

---

<div class="post-metadata">

**Author:** ![filchristou](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/filchristou/32/26760_2.png) [@filchristou](https://discourse.julialang.org/u/filchristou)\
**Post date:** [February 10, 2022, 9:00am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/13 "2022-02-10T09:00:36Z")

</div>

Thanks for your notebook. I run a sweep across 1 to 50 distinct types to see how this affect the latency.  
This is the code used, compliant to your notebook:

```julia
begin
	bst = Vector{Float64}()
	bdd_sab = Vector{Float64}()
	bdd_any = Vector{Float64}()
	bmd_sab = Vector{Float64}()
	bmd_any = Vector{Float64}()
	bus = Vector{Float64}()
	b_dt = Vector{Float64}()
	bus_dt = Vector{Float64}()
	for k in 1:50
		sab_collection=Vector{Sab}([allS[i % k + 1]() for i=1:N])
		any_collection=Vector{Any}([allS[i % k + 1]() for i=1:N])
		single_collection=[allS[1]() for i=1:N]
		#reference - single type
		push!(bst, mean(@benchmark $sumup_f($single_collection)).time)
		#dynamic dispatch
		push!(bdd_sab, mean(@benchmark $sumup_f($sab_collection)).time)
		push!(bdd_any, mean(@benchmark $sumup_f($any_collection)).time)
		# manual dispatch
		push!(bmd_sab, mean(@benchmark $sumup_f_manual($sab_collection)).time)
		push!(bmd_any, mean(@benchmark $sumup_f_manual($any_collection)).time)
		#union splitting
		UnionS=Union{allS[1:k]...}
		unionS_collection=UnionS[o for o ∈ any_collection]
		push!(bus, mean(@benchmark $sumup_f($unionS_collection)).time)
		## collection of type
		datatype_collection=[allS[i % k + 1] for i=1:N]
		# dynamic dispatch
		push!(b_dt, mean(@benchmark $sumup_f($datatype_collection)).time)
		UnionTS=Union{[Type{s} for s in allS[1:k]]...}
		unionTS_collection=UnionTS[o for o ∈ datatype_collection]
		typeof(unionTS_collection)
		# union splitting
		push!(bus_dt, mean(@benchmark $sumup_f($unionTS_collection)).time)
	end
end

```

And these are the results:

 ![image](https://global.discourse-cdn.com/julialang/original/3X/a/6/a6c421e8f44f15c10c30f0b5806031ea3a1fc017.png)  
and a closer look for the non-dynamic dispatch ones:  
 ![image](https://global.discourse-cdn.com/julialang/original/3X/3/b/3b9967e199bf55646b6ecd9b1e5982bc0b2c2375.png)

It appears that datatype doesn’t scale good.  
I didn’t try it with the functions design because I am more interested in structs that could contain some information.

Apparently it looks that manual dispatch latency grows up to 20 districts types and then remains steady up until 50 distinct types that was tested.

**Union splitting does excellent** and very close to the reference single type delay from beginning to end.

Probably it would be nice to generalize with metaprogramming and test up to thousands of distinct types.  
Also I am not sure what happens when structs start containig some information the functions act upon, because now the structs used are actually singletons with a single state.

---

<div class="post-metadata">

**Author:** ![j-fu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/j-fu/32/11373_2.png) [@j-fu](https://discourse.julialang.org/u/j-fu)\
**Post date:** [February 10, 2022, 12:00pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/14 "2022-02-10T12:00:55Z")

</div>

Hi, tried to setup something with metaprogramming here:

```julia

using BenchmarkTools
NT=50
N=10_000
const alltypes=Any[]

@info "Create $(NT) types and methods:"
@time for i=1:NT
    tname=Symbol("T"*string(i))
    ex=quote 
        struct $tname end
        f(::$tname,x)=$i*x
        push!(alltypes,$tname)
    end
    eval(ex)
end

@info "Create union of $NT types:"
@time const UnionT=Union{alltypes...}

function sumup_f(collection)
	s=0.0
	for i=1:length(collection)
		s+=f(collection[i],1)
	end
	s
end

@info "Create collection of Any of size $N:"
@time any_collection=[rand(alltypes)() for i=1:N]

@info "Create collection of UnionT of size $N:"
@time union_collection=UnionT[rand(alltypes)() for i=1:N]
    

@info "Sum up collection of Any:"
@btime sumup_f($any_collection)

@info "Sum up collection of UnionT:"
@btime sumup_f($union_collection)

```

I get as a result:

```julia
[ Info: Create 50 types and methods:
  0.040379 seconds (14.62 k allocations: 1004.555 KiB, 8.87% compilation time)
[ Info: Create union of 50 types:
  0.000060 seconds (58 allocations: 1.828 KiB)
[ Info: Create collection of Any of size 10000:
  0.071567 seconds (233.06 k allocations: 15.093 MiB, 96.25% compilation time)
[ Info: Create collection of UnionT of size 10000:
  2.545888 seconds (3.76 M allocations: 224.753 MiB, 1.12% gc time, 98.88% compilation time)
[ Info: Sum up collection of Any:
  1.051 ms (10000 allocations: 156.25 KiB)
[ Info: Sum up collection of UnionT:
  76.302 μs (0 allocations: 0 bytes)

```

Union splitting works well again, but the bottleneck in this code is “Create collection of UnionT of size 10000:” which possibly may be improved by some trickery.

In our case, indeed, we have large collections of not that many types (or functions) so the pattern seems to work well. However it seems to be not documented, so some confirmation appears to be needed.

---

<div class="post-metadata">

**Author:** ![j-fu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/j-fu/32/11373_2.png) [@j-fu](https://discourse.julialang.org/u/j-fu)\
**Post date:** [February 10, 2022, 8:09pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/15 "2022-02-10T20:09:19Z")

</div>

Did some archeology:

```julia
Julia 1.0.5:
Any: 556.432 μs (10000 allocations: 156.25 KiB)
UnionT: 699.802 μs (10000 allocations: 156.25 KiB

Julia 1.3.1:
Any: 590.025 μs (10000 allocations: 156.25 KiB)
UnionT: 699.267 μs (10000 allocations: 156.25 KiB)

Julia 1.4.2:
Any: 567.394 μs (10000 allocations: 156.25 KiB)
UnionT: 708.185 μs (10000 allocations: 156.25 KiB)

Julia 1.5.2:
Any: 645.280 μs (10000 allocations: 156.25 KiB)
UnionT: 732.489 μs (10000 allocations: 156.25 KiB)

Julia 1.6.0:
Any: 1.145 ms (10000 allocations: 156.25 KiB)
UnionT: 73.300 μs (0 allocations: 0 bytes)

Julia 1.6.5:
Any: 1.066 ms (10000 allocations: 156.25 KiB)
UnionT: 72.818 μs (0 allocations: 0 bytes)

Julia 1.7.0:
Any: 1.124 ms (10000 allocations: 156.25 KiB)
UnionT: 8.710 μs (0 allocations: 0 bytes) (sic!)

```

So this feature seems to be there since 1.6 - correlating with the fact that [#37378](https://github.com/JuliaLang/julia/pull/37378) went [into 1.6.0-beta1](https://github.com/JuliaLang/julia/commit/2a36f83ce9bdfd17c572472f60b830aedcf181f0).

Not sure what made the other factor 10 possible for 1.7, but this is a good thing…

---

<div class="post-metadata">

**Author:** ![sylvaticus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sylvaticus/32/203883_2.png) [@sylvaticus](https://discourse.julialang.org/u/sylvaticus)\
**Post date:** [February 10, 2022, 8:45pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/16 "2022-02-10T20:45:50Z")

</div>

And why with `any` the time instead doubled? Is it some benchmarking issue? Or some trade-off to allow the improvement with the union?

---

<div class="post-metadata">

**Author:** ![j-fu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/j-fu/32/11373_2.png) [@j-fu](https://discourse.julialang.org/u/j-fu)\
**Post date:** [February 10, 2022, 9:19pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/17 "2022-02-10T21:19:19Z")

</div>

I don’t know. Surely not a benchmarking issue. I posted a link to this discussion in the slack (julia/general), may be we will get an answer there.

---

<div class="post-metadata">

**Author:** ![j-fu](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/j-fu/32/11373_2.png) [@j-fu](https://discourse.julialang.org/u/j-fu)\
**Post date:** [February 11, 2022, 3:31pm UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/18 "2022-02-11T15:31:25Z")

</div>

[https://github.com/JuliaLang/julia/pull/44131](https://github.com/JuliaLang/julia/pull/44131)

---

<div class="post-metadata">

**Author:** ![filchristou](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/filchristou/32/26760_2.png) [@filchristou](https://discourse.julialang.org/u/filchristou)\
**Post date:** [February 16, 2022, 8:16am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/19 "2022-02-16T08:16:52Z")

</div>

> [@filchristou](#):
>
> Also I am not sure what happens when structs start containig some information the functions act upon, because now the structs used are actually singletons with a single state.

As mention I think the results might be misleading. We actually measure only the case of dispatching and not really acting on any “Abstract” data.  
I tried the following simple code and it seems that the bottleneck is still there for the general case:

```julia
using BenchmarkTools
using Random
using GLMakie

abstract type MyAbstractType end

struct MyInt1 <: MyAbstractType
    x::Int
end
struct MyInt2 <: MyAbstractType
    x::Int
end
struct MyInt3 <: MyAbstractType
    x::Int
end
struct MyInt4 <: MyAbstractType
    x::Int
end
struct MyInt5 <: MyAbstractType
    x::Int
end
struct MyInt6 <: MyAbstractType
    x::Int
end
struct MyInt7 <: MyAbstractType
    x::Int
end
struct OtherInt4
    x::Int
end

function mysum(mv::Vector{T}) where T
    sum([v.x for v in mv])
end

bab = Vector{Float64}()
bany = Vector{Float64}()
bus = Vector{Float64}()

vsref = fill(MyInt1(1), 1000);
# bvs = mean(@benchmark(mysum(vsref))).time

vsab = Vector{MyAbstractType}(fill(MyInt1(1), 1000));
push!(bab , mean(@benchmark(mysum(vsab))).time)

vsany = Vector{MyAbstractType}(fill(MyInt1(1), 1000));
push!(bany , mean(@benchmark(mysum(vsany))).time)

vsus = Vector{Union{MyInt1}}(fill(MyInt1(1), 1000));
push!(bus , mean(@benchmark(mysum(vsus))).time)

# 2 types
#
vs2ab = Vector{MyAbstractType}(vcat(fill(MyInt1(1), 500),fill(MyInt2(1), 500)));
push!(bab , mean(@benchmark(mysum(vsany))).time)

vs2any = Vector{Any}(vcat(fill(MyInt1(1), 500),fill(MyInt2(1), 500)));
push!(bany , mean(@benchmark(mysum(vs2any))).time)

vs2us = Vector{Union{MyInt1, MyInt2}}(vcat(fill(MyInt1(1), 500),fill(MyInt2(1), 500)));
push!(bus, mean(@benchmark(mysum(vs2us))).time)

# 3 types
#
vs3ab = Vector{MyAbstractType}(vcat(fill(MyInt1(1), 334),fill(MyInt2(1), 333), fill(MyInt3(1), 333)));
push!(bab , mean(@benchmark(mysum(vs3any))).time)

vs3any = Vector{Any}(vcat(fill(MyInt1(1), 334),fill(MyInt2(1), 333), fill(MyInt3(1), 333)));
push!(bany , mean(@benchmark(mysum(vs3any))).time)

vs3us = Vector{Union{MyInt1, MyInt2, MyInt3}}(vcat(fill(MyInt1(1), 334),fill(MyInt2(1), 333), fill(MyInt3(1), 333)));
push!(bus , mean(@benchmark(mysum(vs3us))).time)

# 4 types
#
vs4ab = Vector{MyAbstractType}(vcat(fill(MyInt1(1), 250),fill(MyInt2(1), 250), fill(MyInt3(1), 250), fill(MyInt4(1), 250)));
push!(bab , mean(@benchmark(mysum(vs4any))).time)

vs4any = Vector{Any}(vcat(fill(MyInt1(1), 250),fill(MyInt2(1), 250), fill(MyInt3(1), 250), fill(MyInt4(1), 250)));
push!(bany , mean(@benchmark(mysum(vs4any))).time)

vs4us = Vector{Union{MyInt1, MyInt2, MyInt3, MyInt4}}(vcat(fill(MyInt1(1), 250),fill(MyInt2(1), 250), fill(MyInt3(1), 250), fill(MyInt4(1), 250)));
push!(bus , mean(@benchmark(mysum(vs4us))).time)

# 5 types
#
vs5ab = Vector{MyAbstractType}(vcat(fill(MyInt1(1), 200),fill(MyInt2(1), 200), fill(MyInt3(1), 200), fill(MyInt4(1), 200), fill(MyInt5(1), 200)));
push!(bab , mean(@benchmark(mysum(vs4any))).time)

vs5any = Vector{Any}(vcat(fill(MyInt1(1), 200),fill(MyInt2(1), 200), fill(MyInt3(1), 200), fill(MyInt4(1), 200), fill(MyInt5(1), 200)));
push!(bany , mean(@benchmark(mysum(vs5any))).time)

vs5us = Vector{Union{MyInt1, MyInt2, MyInt3, MyInt4, MyInt5}}(vcat(fill(MyInt1(1), 200),fill(MyInt2(1), 200), fill(MyInt3(1), 200), fill(MyInt5(1), 200), fill(MyInt4(1), 200)));
push!(bus , mean(@benchmark(mysum(vs5us))).time)

# 6 types
#
vs6ab = Vector{MyAbstractType}(vcat(fill(MyInt1(1), 170),fill(MyInt2(1), 166), fill(MyInt3(1), 166), fill(MyInt4(1), 166), fill(MyInt5(1), 166), fill(MyInt6(1), 166)));
push!(bab , mean(@benchmark(mysum(vs6ab))).time)

vs6any = Vector{Any}(vcat(fill(MyInt1(1), 170),fill(MyInt2(1), 166), fill(MyInt3(1), 166), fill(MyInt4(1), 166), fill(MyInt5(1), 166), fill(MyInt6(1), 166)));
push!(bany , mean(@benchmark(mysum(vs6any))).time)

vs6us = Vector{Union{MyInt1, MyInt2, MyInt3, MyInt4, MyInt5, MyInt6}}(vcat(fill(MyInt1(1), 170),fill(MyInt2(1), 166), fill(MyInt3(1), 166), fill(MyInt4(1), 166), fill(MyInt5(1), 166), fill(MyInt6(1), 166)));
push!(bus , mean(@benchmark(mysum(vs6us))).time)
# Plot

# 7 types
#
vs7ab = Vector{MyAbstractType}(vcat(fill(MyInt1(1), 148),fill(MyInt2(1), 142), fill(MyInt3(1), 142), fill(MyInt4(1), 142), fill(MyInt5(1), 142), fill(MyInt6(1), 142), fill(MyInt7(1), 142)));
push!(bab , mean(@benchmark(mysum(vs7ab))).time)

vs7any = Vector{Any}(vcat(fill(MyInt1(1), 148),fill(MyInt2(1), 142), fill(MyInt3(1), 142), fill(MyInt4(1), 142), fill(MyInt5(1), 142), fill(MyInt6(1), 142), fill(MyInt7(1), 142)));
push!(bany , mean(@benchmark(mysum(vs7any))).time)

vs7us = Vector{Union{MyInt1, MyInt2, MyInt3, MyInt4, MyInt5, MyInt6, MyInt7}}(vcat(fill(MyInt1(1), 148),fill(MyInt2(1), 142), fill(MyInt3(1), 142), fill(MyInt4(1), 142), fill(MyInt5(1), 142), fill(MyInt6(1), 142), fill(MyInt7(1), 142)));
push!(bus , mean(@benchmark(mysum(vs7us))).time)

colors=[:blue,:green,:yellow]
f,a,p = barplot(repeat(1:length(bab), outer=3), 
        vcat(bab, bany,bus), 
        dodge= repeat(1:3, inner=length(bab)), 
        color= repeat(1:3, inner=length(bab)),
        axis = (title="Summation of struct fields",
                xticks = (1:length(bab), repr.(1:length(bab))),
                ylabel="nanosecs",
                xlabel="types contained in vector"))
axislegend(a, [PolyElement(polycolor = colors[i]) for i in 1:3], 
           ["Vector{Abstract}","Vector{Any}", "Vector{Union{...}}"], 
    position = :lt, orientation = :vertical)

```

So basically just adding together the fields of different structs belonging to the same abstract type.

Getting the following results which speak for themselves:

 ![image](https://global.discourse-cdn.com/julialang/original/3X/8/e/8e45a1627c9effefbf2c5de14ff61358d35337e9.png)  
The red line is the reference, i.e. time for concrete single type vector.

---

<div class="post-metadata">

**Author:** ![tim.holy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/tim.holy/32/52_2.png) [@tim.holy](https://discourse.julialang.org/u/tim.holy)\
**Post date:** [February 16, 2022, 10:31am UTC](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883/20 "2022-02-16T10:31:38Z")

</div>

It’s useful to distinguish two related concepts:

- [union splitting](https://julialang.org/blog/2018/08/union-splitting/): specializing call sites when you know the type can only be one of a specific, finite list
- what I have been calling [“world splitting”](https://www.youtube.com/watch?v=wXRMwJdEjX4) (specifically the section starting at 6:48, or more narrowly starting at 10:40): specializing call sites for the methods that have been defined at the time of compilation

Keep in mind that the second is open-ended, whereas the former is closed. That’s a huge difference. While “open-ended” gives you greater flexibility and may be necessary in some cases, it opens you to much longer compile times and [invalidation](https://julialang.org/blog/2020/08/invalidations/). Over time, Julia has gotten _more_ restrictive: the world-splitting limit dropped from 4 types to 3 types, which is the reason your tall yellow bars above start once you have 4 methods. This is because the fewer methods you have to speculatively infer, the faster compilation becomes. (IIUC correctly, if you raise the limit to 5, no one has had the patience to wait for Julia to finish compiling itself, and it’s possible it never does.) While I understand that you might wish it were higher “for just this one function,” the reality is that these parameters have huge implications for the usability of Julia in the broader ecosystem. Several of us are quite excited about a [change on master](https://github.com/JuliaLang/julia/pull/43370) that will allow package authors to drop the limit even further (you can cut the TTFP for Plots in half by dropping the limit to 1).

The moral of the story: if you care about performance, you’re best off using real Union-splitting, rather than relying on world-splitting. In cases where you need to intervene, your best option is to add `isa(x, T)` blocks and do the dispatch manually. Your performance will be good here only if `T` is a concrete type, since abstract/partial types will invoke julia’s subtyping machinery, whereas comparing to a concrete type is done by pointers (i.e., very fast).

[Next page](https://discourse.julialang.org/t/avoiding-vectors-of-abstract-types/61883.md?page=2)
