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

MWE:

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?

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

julia> struct S end

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

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:

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

Generic equality operator. Falls back to ===.

8 Likes