15
votes

I'm wondering about immutable types and performances in Julia.

  1. In which case does making a composite type immutable improve perfomances? The documentation says

    They are more efficient in some cases. Types like the Complex example above can be packed efficiently into arrays, and in some cases the compiler is able to avoid allocating immutable objects entirely.

    I don't really understand the second part.

  2. Are there cases where making a composite type immutable reduce performance (beyond the case where a field needs to be changed by reference)? I thought one example could be when an object of an immutable type is used repeatedly as an argument, since

    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.

    However, I can't find any difference in a simple example:

    abstract MyType
    
    type MyType1 <: MyType
        v::Vector{Int}
    end
    
    immutable MyType2 <: MyType
        v::Vector{Int}
    end
    
    
    g(x::MyType) = sum(x.v)
    
    function f(x::MyType)
        a = zero(Int)
        for i in 1:10_000
            a += g(x)
        end
        return a
    end
    
    x = fill(one(Int), 10_000)
    x1 = MyType1(x)
    @time f(x1)
    # elapsed time: 0.030698826 seconds (96 bytes allocated)
    x2 = MyType2(x)
    @time f(x2)
    # elapsed time: 0.031835494 seconds (96 bytes allocated)
    

    So why isn't f slower with an immutable type? Are there cases where using immutable types makes a code slower?

2
You are not doing anything with those, you are just timing the constructor methods.HarmonicaMuse
From what I understand your MyType2, although immutable, still contains a reference to the vector in the v field, only the reference cannot change. The vector is mutable so you can do stuff like: x = MyType2([1:4]); x.v[1] = 5. This operation is in-place so it doesn't change the reference. So in both type cases the vector in v should be passed by reference.mmagnuski

2 Answers

10
votes

Immutable types are especially fast when they are small and consist entirely of immediate data, with no references (pointers) to heap-allocated objects. For example, an immutable type that consists of two Ints can potentially be stored in registers and never exist in memory at all.

Knowing that a value won't change also helps us optimize code. For example you access x.v inside a loop, and since x.v will always refer to the same vector we can hoist the load for it outside the loop instead of re-loading on every iteration. However whether you get any benefit from that depends on whether that load was taking a significant fraction of the time in the loop.

It is rare in practice for immutables to slow down code, but there are two cases where it might happen. First, if you have a large immutable type (say 100 Ints) and do something like sorting an array of them where you need to move them around a lot, the extra copying might be slower than pointing to objects with references. Second, immutable objects are usually not allocated on the heap initially. If you need to store a heap reference to one (e.g. in an Any array), we need to move the object to the heap. From there the compiler is often not smart enough to re-use the heap-allocated version of the object, and so might copy it repeatedly. In such a case it would have been faster to just heap-allocate a single mutable object up front.

2
votes

This test includes a special cases, so is not extendable and could not reject better performance of immutable types.
check following test and look at different allocation times,when create a vector of immutables compare to a vector of mutables

abstract MyType
type MyType1 <: MyType
    i::Int
    b::Bool
    f::Float64
end
immutable MyType2 <: MyType
    i::Int
    b::Bool
    f::Float64
end

@time x=[MyType2(i,1,1) for i=1:100_000];
# => 0.001396 seconds (2 allocations: 1.526 MB)
@time x=[MyType1(i,1,1) for i=1:100_000];
# => 0.003683 seconds (100.00 k allocations: 3.433 MB)