Just to elaborate on the order-dependent type layout, the equal t1 and t2 are (abstract) supertypes of exactly 8 concrete 24-byte Tuple types. The difference is that t1 is a direct Tuple type with 3 Union elements, while t2 is a Union of the 8 Tuple types. Abstract types don’t have direct instances for which to prescribe a memory layout, but Vectors with them as element types are concrete. Most abstract types would make the elements’ data stored separately, with the Vector’s buffer storing only pointers to them (8 bytes on 64-bit systems). Unions of a limited number of isbits types however can be optimized as storing the data directly in the Memory buffer, followed by a sequence of type tags to distinguish the elements’ concrete types.
Julia decided to use the optimization if the type Vector{t2} is made first and let the equal Vector{t1} follow suit, but not in the reverse order. The implementations are observably different, but the point is they’re both correct for programs that reached nearly equivalent global states (you could get closer by swapping a and b in the 1st case for a consistent a::Vector{t1} and b::Vector{t2}). People have complained that Julia would ideally figure out the optimization is possible in either order, but types aren’t as obvious as t1 in general. It’s bad for optimizers to run a long time, possibly forever, so heuristics save time and can be order-dependent. Order-dependent effects could often be eliminated by strengthening the circumstances to the exact same source code, but conditionals and parallelism can still vary evaluation order. Introducing time-dependence to a unified t1 vs t2 example (again, bad idea in practice):
t1 = NTuple{3,Union{Int,Missing}}
t2 = Union{Tuple{Missing,Missing,Missing},
Tuple{Missing,Missing,Int},
Tuple{Missing,Int,Missing},
Tuple{Missing,Int,Int},
Tuple{Int,Missing,Missing},
Tuple{Int,Missing,Int},
Tuple{Int,Int,Missing},
Tuple{Int,Int,Int}}
# system clock seconds determines order of evaluating Vector types
if iseven(round(Int, time()))
Vector{t1}, Vector{t2}
else
Vector{t2}, Vector{t1}
end
# order of Vector instantiation doesn't matter here
a = Vector{t1}(undef, 1)
b = Vector{t2}(undef, 1)
@show Vector{t1}==Vector{t2} sizeof(a) sizeof(b)
and a flattened CLI expression for convenient repetition across different processes:
julia -e "t1 = NTuple{3,Union{Int,Missing}}; t2 = Union{Tuple{Missing,Missing,Missing},Tuple{Missing,Missing,Int}, Tuple{Missing,Int,Missing}, Tuple{Missing,Int,Int}, Tuple{Int,Missing,Missing}, Tuple{Int,Missing,Int}, Tuple{Int,Int,Missing}, Tuple{Int,Int,Int}}; if iseven(round(Int,time())); Vector{t1}, Vector{t2}; else; Vector{t2}, Vector{t1}; end; a= Vector{t1}(undef, 1); b= Vector{t2}(undef, 1); @show Vector{t1}==Vector{t2} sizeof(a) sizeof(b)"
As for strengthening the circumstances to the exact same evaluation order of Julia objects, I don’t recall and can’t imagine an example where the instructions and static data can vary. It’s possible that it really doesn’t for us, but I have read about exotic pseudorandom heuristics, which get random seeds from the system like the time-dependent examples here getting system times.
I do have a question on my mind now though: if 2+ packages are precompiled with different representations and code for Vector{t1}, how are they reconciled in the same environment or when I import them into the same session?