# Why does \`Union{Int,...}\` in a \`Vector\` not cause allocations but \`Union{Float64,...}\` does?

**URL:** https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067
**Category:** Performance
**Tags:** memory-allocation
**Created:** [September 3, 2025, 7:36am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067 "2025-09-03T07:36:36Z")
**Posts on this page:** 9
**Page:** 1

<div class="post-metadata">

### Author: ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)
#### Post date: [September 3, 2025, 7:36am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/1 "2025-09-03T07:36:36Z")

</div>

I have a custom missing type which stores a symbol and a reference to an object whose type is not predetermined, so it’s an Any field. I need to store vectors of these, some values will be missing but most won’t be. Here’s a test function that looks at the allocations that happen when writing values and missings into such a vector.

The peculiar thing to me is that when I write ten missings unioned with `Int`, I get around 10 allocations, so one per missing. But if I union with `Float64`, every value in the vector needs an allocation. Why is the storage behavior different if both `Int` and `Float64` are primitives with 8 bytes size?

```julia
struct MyMissing
  location::Symbol
  reason
end

function missing_vector(T, n)
  v = Vector{Union{MyMissing,T}}(undef, n)
  for i in 1:n
    v[i] = i > 10 ? T(i) : MyMissing(:missing_vector, "under ten")
  end
  return count(x -> x isa MyMissing, v)
end

```

```julia-auto
julia> @time missing_vector(Int, 100)
  0.000008 seconds (12 allocations: 1.219 KiB)
10

julia> @time missing_vector(Float64, 100)
  0.000018 seconds (102 allocations: 2.625 KiB)
10

```

---

<div class="post-metadata">

### Author: ![jakobnissen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jakobnissen/32/13477_2.png) [@jakobnissen](https://discourse.julialang.org/u/jakobnissen)
#### Post date: [September 3, 2025, 8:15am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/2 "2025-09-03T08:15:17Z")

</div>

Julia seems to have special codegen for boxing integers specifically: The `Float64` calls `ijl_gc_small_alloc`, the `Int` case calls `ijl_box_int64`.

---

<div class="post-metadata">

### Author: ![Sukera](https://avatars.discourse-cdn.com/v4/letter/s/ce7236/32.png) [@Sukera](https://discourse.julialang.org/u/Sukera)
#### Post date: [September 3, 2025, 8:17am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/3 "2025-09-03T08:17:59Z")

</div>

Does this mean that boxed integers aren’t counted for allocations?

---

<div class="post-metadata">

### Author: ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)
#### Post date: [September 3, 2025, 8:18am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/4 "2025-09-03T08:18:03Z")

</div>

> [@jakobnissen](#):
>
> Julia seems to have special codegen for boxing integers specifically: The `Float64` calls `ijl_gc_small_alloc`, the `Int` case calls `ijl_box_int64`.

Oh yeah I heard somewhere that Julia has a cache of small `Int`s pre-boxed as an optimisation. So in your code no allocation needs to happen for the `Int`s.

However if you replace the code with `T(100000+i)` or make the loop bigger the difference should go away.

---

<div class="post-metadata">

### Author: ![jakobnissen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jakobnissen/32/13477_2.png) [@jakobnissen](https://discourse.julialang.org/u/jakobnissen)
#### Post date: [September 3, 2025, 8:20am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/5 "2025-09-03T08:20:49Z")

</div>

Yeah, indeed the difference goes away when bigger integers are used:

```julia
julia> function missing_vector(T, n)
         v = Vector{Union{MyMissing,T}}(undef, n)
         for i in 10000000:n+10000000-1
           v[i - 10000000 + 1] = i > 10 ? T(i) : MyMissing(:missing_vector, "under ten")
         end
       end
missing_vector (generic function with 1 method)

julia> @time missing_vector(Int, 100)
  0.000003 seconds (101 allocations: 2.438 KiB)

```

---

<div class="post-metadata">

### Author: ![jules](https://avatars.discourse-cdn.com/v4/letter/j/41988e/32.png) [@jules](https://discourse.julialang.org/u/jules)
#### Post date: [September 3, 2025, 8:30am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/6 "2025-09-03T08:30:28Z")

</div>

Oh interesting, pre-boxed integers.. Well then I picked a really unfortunate example when I checked early on if my union approach was ok in terms of allocation behavior. I thought that julia could store unions inline with type tags but maybe only if they are all isbits.

But overall it’s still very fast even allocating millions of these small values. So practically it probably doesn’t matter in my case.

---

<div class="post-metadata">

### Author: ![cjdoris](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/cjdoris/32/213133_2.png) [@cjdoris](https://discourse.julialang.org/u/cjdoris)
#### Post date: [September 3, 2025, 9:33am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/7 "2025-09-03T09:33:06Z")

</div>

As always, profile realistic uses of your code to identify the actual bottlenecks before trying to optimise anything.

`Vector` does have a more efficient storage for unions than boxing, but only for really small unions like `Union{Nothing,Int}`. I think it internally stores just the `Int`s inline plus has a `BitVector` to store which ones are actually `Nothing`.

---

<div class="post-metadata">

### Author: ![jakobnissen](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jakobnissen/32/13477_2.png) [@jakobnissen](https://discourse.julialang.org/u/jakobnissen)
#### Post date: [September 3, 2025, 11:29am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/8 "2025-09-03T11:29:25Z")

</div>

That’s only for small unions where every subtype is isbits. It would be really nice to have it for pointer-ful (i.e. GC-tracked) types also. I believe that would enable implementing something like [smallvec](https://docs.rs/smallvec/latest/smallvec/).

A related limitation is that a function that returns a union with a pointer-ful type can’t store its return values on the stack, but must heap-allocate it. This can be a problem if you lean heavily into error return types such that your code often returns unions.

I’m not sure whether these two limitations (Memory storage versus ABI for pointerful unions) are the same.

---

<div class="post-metadata">

### Author: ![pdeffebach](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/pdeffebach/32/10320_2.png) [@pdeffebach](https://discourse.julialang.org/u/pdeffebach)
#### Post date: [September 3, 2025, 11:44am UTC](https://discourse.julialang.org/t/why-does-union-int-in-a-vector-not-cause-allocations-but-union-float64-does/132067/9 "2025-09-03T11:44:22Z")

</div>

You might be interested in [this](https://github.com/nalimilan/TypedMissings.jl) package by @nalimilan
