Arrays of structs/tuples with mutable members

I tested the performance of an array of tuples and structs in Julia 1.3.1 and 1.5.1 and am a bit confused about the results. There are eight separate times based on three choices: 1.3.1 vs 1.5.1, structs vs tuples, and mutable member vs no mutable members.

Briefly: 1.5.1 is much faster than 1.3.1 in the case of a mutable member for both structs and tuples. The timing results and code are below.

I have a question about this: For the 1.5.1 execution, the timing results imply that structs and tuples are not heap-allocated even when they contain a mutable member. How does the run-time system keep track of whether the underlying array [72., 31.] has any live pointers to it (for garbage collection)? Is there any “gotcha” in using this new feature?

julia> include("testtup1.jl")
WARNING: replacing module testtup1.
VERSION = 1.3.1
Test of tuple with mutable member
  0.203990 seconds (25.00 M allocations: 762.940 MiB, 12.14% gc time)
Test of struct with mutable member
  0.200921 seconds (25.00 M allocations: 762.940 MiB, 9.90% gc time)
Test of tuple with all immutable members
  0.065874 seconds (7 allocations: 416 bytes)
Test of struct with all immutable members
  0.048031 seconds (7 allocations: 416 bytes)
Main.testtup1
julia> include("testtup1.jl")
WARNING: replacing module testtup1.
VERSION = 1.5.1
Test of tuple with mutable member
  0.058091 seconds (2 allocations: 176 bytes)
Test of struct with mutable member
  0.056815 seconds (2 allocations: 176 bytes)
Test of tuple with all immutable members
  0.060029 seconds (2 allocations: 208 bytes)
Test of struct with all immutable members
  0.048437 seconds (2 allocations: 208 bytes)
Main.testtup1

module testtup1

struct Example{T}
    a::T
    b::Int
end


function multiple_push_pop_tup(u)
    T = Tuple{typeof(u), Int}
    x = Vector{T}()
    resize!(x,5)
    sum1 = 0.0
    for k = 1 : 5_000_000
        k1 = (k % 2) + 1
        for i = 1 : 5
            x[i]= (u,k1)
        end
        for i = 1 : 5
            v = x[i]
            sum1 += v[1][v[2]]
        end
    end
    return x[2], sum1
end

function multiple_push_pop_struct(u)
    T = Example{typeof(u)}
    x = Vector{T}()
    resize!(x,5)
    sum1 = 0.0
    for k = 1 : 5_000_000
        k1 = (k % 2) + 1
        for i = 1 : 5
            x[i]= T(u,k1)
        end
        for i = 1 : 5
            v = x[i]
            sum1 += v.a[v.b]
        end
    end
    return x[2], sum1
end

const w = [72., 31.]
multiple_push_pop_tup(w)
multiple_push_pop_struct(w)
const y = (72., 31.)
multiple_push_pop_tup(y)
multiple_push_pop_struct(y)

println("VERSION = ", VERSION)
println("Test of tuple with mutable member")
@time multiple_push_pop_tup(w)
println("Test of struct with mutable member")
@time multiple_push_pop_struct(w)
println("Test of tuple with all immutable members")
@time multiple_push_pop_tup(y)
println("Test of struct with all immutable members")
@time multiple_push_pop_struct(y)
end

(Sorry about my initial post in which the timing was done incorrectly; I think I have revised it now correctly.)

No. Garbage collection on the mutable members works normally, no different than for an array of those members — the gc still knows it has a pointer to the members in the array data.

1 Like

Thank you very much for the prompt response, Steven. In the documentation for sorted containers here: https://juliacollections.github.io/DataStructures.jl/latest/sorted_containers/#Tokens-for-Sorted-Containers-1, there is a discussion in the fourth paragraph about performance of tokens and semitokens. It seems like this section of the documentation needs to be scrapped? Maybe semitokens themselves should be discarded?

Yes, it sounds like that’s only applicable to Julia < 1.5.