# Mutable structs with all constant fields outperform immutable structs for equality comparison

**URL:** <https://discourse.julialang.org/t/mutable-structs-with-all-constant-fields-outperform-immutable-structs-for-equality-comparison/123430>\
**Category:** Performance\
**Tags:** struct\
**Created:** [December 3, 2024, 11:47pm UTC](https://discourse.julialang.org/t/mutable-structs-with-all-constant-fields-outperform-immutable-structs-for-equality-comparison/123430 "2024-12-03T23:47:03Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![frankwswang](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/frankwswang/32/18561_2.png) [@frankwswang](https://discourse.julialang.org/u/frankwswang)\
**Post date:** [December 3, 2024, 11:47pm UTC](https://discourse.julialang.org/t/mutable-structs-with-all-constant-fields-outperform-immutable-structs-for-equality-comparison/123430/1 "2024-12-03T23:47:03Z")

</div>

MWE:

```julia
julia> x1 = [rand(Int) for _=1:1000];

julia> x2 = [rand(Int) for _=1:1000];

julia> v1 = [rand(3,3) for _=1:1000];

julia> v2 = [rand(3,3) for _=1:1000];

julia> struct myT
       a::Int
       b::Any
       end

julia> mutable struct myT2
       const a::Int
       const b::Any
       end

julia> using BenchmarkTools

julia> k1s = myT.(x1, v1);

julia> k2s = myT.(x2, v2);

julia> @benchmark $k1s .== $k2s
BenchmarkTools.Trial: 10000 samples with 9 evaluations.
 Range (min … max): 2.378 μs … 22.189 μs ┊ GC (min … max): 0.00% … 0.00%
 Time (median): 2.856 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 2.882 μs ± 552.067 ns ┊ GC (mean ± σ): 0.00% ± 0.00%

                ▁ █ ▁▁
  ▂▂▂▃▃▃▃▅▃▅▃▄▃▅█▆▅▅▇█▆██▄▅▃▂▃▂▂▂▃▂▃▃▂▃▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▁▂ ▃
  2.38 μs Histogram: frequency by time 3.86 μs <

 Memory estimate: 224 bytes, allocs estimate: 3.

julia> l1s = myT2.(x1, v1);

julia> l2s = myT2.(x2, v2);

julia> @benchmark $l1s .== $l2s
BenchmarkTools.Trial: 10000 samples with 172 evaluations.
 Range (min … max): 625.581 ns … 8.904 μs ┊ GC (min … max): 0.00% … 91.64%
 Time (median): 687.209 ns ┊ GC (median): 0.00%
 Time (mean ± σ): 717.957 ns ± 291.679 ns ┊ GC (mean ± σ): 1.68% ± 4.59%

    ▃ ▂█▃
  ████████▅▃▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ▂
  626 ns Histogram: frequency by time 1.36 μs <

 Memory estimate: 224 bytes, allocs estimate: 3.

```

If my understanding is correct, this performance difference is because `myT2` can directly store a pointer to `.b` along with `.a` to form a better memory layout. My question is, can the compiler automatically convert the lower-layer implementation of `myT` to `myT2` in the future?

---

<div class="post-metadata">

**Author:** ![nsajko](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nsajko/32/221187_2.png) [@nsajko](https://discourse.julialang.org/u/nsajko)\
**Post date:** [December 4, 2024, 12:13am UTC](https://discourse.julialang.org/t/mutable-structs-with-all-constant-fields-outperform-immutable-structs-for-equality-comparison/123430/2 "2024-12-04T00:13:04Z")

</div>

You didn’t define `==` for the new types, though. The default `==` method just forwards to `===`:

```julia-repl
julia> struct S end

julia> @which S() == S()
==(x, y)
     @ Base Base.jl:207

```

> <https://github.com/JuliaLang/julia/blob/v1.11.2/base/Base.jl#L207-L207>

So it comes down to `===` being cheaper for the `mutable struct`, because it’s implemented as a simple integer comparison, unlike for the immutable `struct`, where the `===` is recursive. This is documented, the doc string of `===` starts with:

> Determine whether `x` and `y` are identical, in the sense that no program could distinguish them. First the types of `x` and `y` are compared. If those are identical, mutable objects are compared by address in memory and immutable objects (such as numbers) are compared by contents at the bit level.

Regarding the unfortunate default method of `==`, here are some Github issues:

> <https://github.com/JuliaLang/julia/issues/4648>
>
> This doesn't make much sense:
> 
> \`\`\` julia
> julia\> immutable Foo{T}
> bar::T…
> end
> 
> julia\> Foo("baz") == Foo("baz")
> false
> 
> julia\> Foo("baz").bar == Foo("baz").bar
> true
> 
> julia\> Foo(1) == Foo(1)
> true
> \`\`\`
> 
> If the fields are \`==\` to each other then the objects should be \`==\` to each other.

> <https://github.com/JuliaLang/julia/issues/40717>
>
> I often make mistakes by using \`==\` between two objects that I shouldn't be comp…aring, such as comparing a number to an array of numbers or comparing a string to a character. The \`==\` comparison evaluates to \`false\`, but it wasn't the comparison I meant to make. For example,
> 
> \`\`\`jl
> a = \[1,2,3\]
> b = 5
> 
> 
> if a == b; # evaluates to false
> \`\`\`
> 
> I wish there were an equality-checking operator that would give an error for incompatible types, like in the example above. I'm not sure if the right implementation would be 
> 
> \- check if they have exactly the same type
> \- check some \`\<:\` subtyping relationship
> \- check if they can \`convert\` into each other's types
> \- check if they can \`promote\` into a shared type
> 
> or something else, or multiple different operators for different purposes. There are plenty of equals-like operators in unicode (eg ≟, ≐). Regardless, the current situation where my mistakes result in silent \`false\`s causes me problems too often for comfort; I would much rather get an explicit error that I can correct. 
> 
> What would be a good solution here?

The behavior is documented, though, the `==` doc string starts with:

> Generic equality operator. Falls back to [`===`](https://docs.julialang.org/en/v1/base/base/#Core.:===).
