I’ll give a stab at answer this. I want to be upfront that I’m not an expert Julia user; hopefully I can shed some light, but this is as much to educate myself as anything else. Hopefully someone who actually knows what they’re talking about can correct any mistakes.
- immutable types can be bits types:
julia> type MutFloat
x::Float64
end
julia> immutable ImmFloat
x::Float64
end
julia> isbits(MutFloat)
false
julia> isbits(ImmFloat)
true
- Let’s compare the performance of these two types. Additionally, we’ll include an immutable type wrapping an array.
immutable ArrFloat
x::Vector{Float64}
end
floats = rand(100000)
immfloats = map(ImmFloat, floats)
mutfloats = map(MutFloat, floats)
arrfloats = map(x -> ArrFloat([x]), floats)
println("Immutable float")
println(@benchmark sum(t.x for t in immfloats))
println("Mutable float")
println(@benchmark sum(t.x for t in mutfloats))
println("Immutable-wrapped array")
println(@benchmark sum(t.x[1] for t in arrfloats))
Immutable float
Trial(97.806 ÎĽs)
Mutable float
Trial(110.693 ÎĽs)
Immutable-wrapped array
Trial(936.762 ÎĽs)
So empirically, there is a slight performance advantage to have immutables. Wrapping in an array is really bad for performance.
What’s going on here? I think with an immutable whose fields are a known leaf type - like ImmFloat
in our example - the data passed around are exactly the bits of the fields. So passing an ImmFloat
is just passing the 64 bits that make up a Float64
; there are not references or redirection. Therefore, if someone gives you an ImmFloat
, operating on its value is easy: just look at the bits they handed you.
For mutable types, however, we pass around references. (From the manual: “An object with an immutable type is passed around (both in assignment statements and in function calls) by copying, whereas a mutable type is passed around by reference.”) So if I hand you a MutFloat
, you’re not holding the bits of the float itself; you’re holding the address where those bits can be found. To operate on the MutFloat
, you have to go where the address tells you to find the bits. This is slower than the corresponding operation on a ImmFloat
.
In the last case, what happens when I give you an immutable type holding an array? I’ve given you exactly the address to that array (whereas if I had given you a mutable type, you’d be holding the address to look for the array’s address). But to actually get the Float64
data out of the array, you have to call getindex
which is comparatively slow - bounds checking, function call, etc.
So punchline here is if you want mutability, use a mutable type.
- I think there may be psychological benefits to immutable types. We tend to use immutables when we’ve carefully thought about what types our data will have and which parts of our programs are invariant. Type specificity is really important for performance:
immutable ImmFloatInt
x::Union{Float64, Int}
end
immfloatints = map(ImmFloatInt, floats)
println("Immutable union")
println(@benchmark sum(t.x for t in immfloatints))
Immutable union
Trial(3.453 ms)
Here, adding up the same 100,000 floating point numbers is about 35x faster if they’re held in ImmFloats
rather than ImmFloatInts
, the latter of which has a union type. (I believe union performance may be improving in the near future, not sure; this is on Julia 0.5.1.)