Reduce GC time

Hello :grin:,
I tried to optimize my code to reduce GC time. Among other things, I tried to implement “in place” operations, in particular with a long GC function “is_intersect!”, which checks whether a given interval intersects with an interval list . However, the following code structure doesn’t change anything to my GC time compared to a “naive” implementation where I create new variable at each step. Do you see an implementations error in the following code?

A structure I used :

using IntervalSets
Base.@kwdef struct S
    id::Int16
    number_of_interval::Int16
    intervals::Vector{ClosedInterval{Float64}}
    ...
end

The is_intersect! function :

function is_intersect!(interval_1::Interval, structure_1::S, is_intersect :: Vector{Bool})
    is_intersect[1] = false
    for interval in structure_1.intervals
        if !isempty(intersect(interval_1, interval))
            is_intersect[1] = true
        end
    end
end

The main function :

function main(structure_1)
    compartment_id = 0
    t = 0.0
    is_intersect = Vector{Bool}(undef, 1)
    time_in_compartment = Vector{Float64}(undef, 1)
    while t < duration
        time_in_compartment[1] = rand(1)[1]

        if compartment_id == 0
            is_intersect!(t .. (t + time_in_compartment[1]), structure_1, is_intersect)

      ...

thanks !
fdekerm

one easy improvement is to use rand()rather than rand(1)[1]

1 Like

Yes, thank you for pointing that out, it was a relic of an earlier version!

Please post enough code so that it can actually be run.

2 Likes

The code is part of a bigger project, so I’ve tried to make a small version that I can share here. The GC time is obviously less marked than on the real version of the code, but do you see areas for improvement/errors in my implementation? For example, does using a mutable struct (struct cell here) and changing the value of its parameters during execution have a strong footprint on the GC?

using IntervalSets
using StatsBase: sample, Weights
using ProgressMeter

function is_intersect!(interval::Interval, fractions_list, is_intersect :: Vector{Bool})
    is_intersect[1] = false
    for fraction in fractions_list
        if !isempty(intersect(interval, fraction))
            is_intersect[1] = true
        end
    end
end

Base.@kwdef mutable struct cell
    const id::Int
    total::Float64 = 0.0 
    actual_compartment::Int16
end


function walk(cell_id, compartment_ids, fractions_list, duration)
    l = cell(id=cell_id,
        actual_compartment=0)

    is_intersect = Vector{Bool}(undef, 1)
    time_in_compartment = Vector{Float64}(undef, 1)
    t = 0.0
    while t < duration
        time_in_compartment[1] = 10 * rand()

        if l.actual_compartment == 0.0
            is_intersect!(t .. (t + time_in_compartment[1]), fractions_list, is_intersect)
            if is_intersect[1] 
                l.total += rand()
            end
        end

        t += time_in_compartment[1]

        l.actual_compartment = sample(compartment_ids)


    end
    return l.total
    
end

number_of_cells = 100_000
compartment_ids = [0,1,2,3,4,5]
fractions_list = [10..12, 20..22, 30..32, 40..42, 50..52, 60..62, 70..72, 80..82, 90..92, 100..102]
duration = 100

function main(number_of_cells, compartment_ids, fractions_list, duration)
    t = Vector{Float64}(undef, number_of_cells)

    p = Progress(number_of_cells) #Progression bar

    @time Threads.@threads for cell_id in 1:number_of_cells

        tot = walk(cell_id, compartment_ids, fractions_list, duration)

        t[cell_id] = tot
        

        next!(p)

    end
    return (t)
end

It seems you allocate twice per cell which should be the two vectors getting allocated inside walk:

    is_intersect = Vector{Bool}(undef, 1)
    time_in_compartment = Vector{Float64}(undef, 1)
1 Like

and would you see any solutions to improve this?

In your current code; I can’t see a reason for using a Vector with a single element. Do you extend this vector in your true code? If not, just use a regular variable, or if you need the mutability (because you want to change the value elsewhere, but that could also be restructured) use a Ref instead. But to me nothing there looks like it allocates way to much.

Let’s take a step back though: How did you check for gctime/allocations? If you used @time it is likely that you included compilation time into your results which also inflates the allocations/gctime since the compiler allocates a lot. The first step to optimization is measuring and then optimizing where the gross of allocations (or time spent in general) happens. If you use vscode you can simply call main once to compile and then do @profview main(...) to get a flamegraph showing where time was spent during execution. Usually that is sufficient for optimization pruposes but you can also track allocations line-by-line as explained here. This will hopefully give you a better understanding of where to optimize and if not you can come back with the data and we will help you decipher it :slight_smile:

3 Likes