Is there a way to mutate a Dates.Date without allocation, such that I don’t put pressure on the garbage collector?
I am reusing an array of structs to reduce GC pressure in an RPC service. I would like to mutate the dates in these structs. I can’t find a function for this in the Dates package. I woiuld prefer to not have to resort to unpacked integers for year, month, day.
If you have some immutable struct that has a Date typed field, and you store those structs in an array, then you can replace a whole entry of this array with a new struct instance of which only the date field has been changed. For that you do not need mutation of a Date or your struct. Packages like GitHub - jw3126/Setfield.jl: Update deeply nested immutable structs. or GitHub - JuliaObjects/Accessors.jl: Update immutable data make creating new instances with changed fields easier. This usually does not allocate and is faster than using mutable data structures.
Interesting packages, thanks. But it looks like that approach will still pressure the garbage collector. Whether I’m creating a new struct or a new date, I’m creating new objects that must be garbage collected.
Replacing one date in a mutable struct with another date in a mutable struct will result in an allocation – for the new Date object. I checked it out in a repl.
If the structs immutable… well then I’m just replacing one struct with another struct, which will result in an allocation – for the new Struct object.
There’s a difference between stack-allocated memory and heap-allocated memory though. Stack memory will not pressure the GC. A DateTime needs memory, yes, but if you already have that memory in an array, the stack memory to instantiate the object doesn’t matter.
julia> using Setfield
julia> using Dates
julia> struct Container
x::Float64
d::DateTime
end
julia> arr = [Container(rand(), now()) for _ in 1:10_000]
10000-element Vector{Container}:
Container(0.45518384534376943, DateTime("2023-05-03T10:50:09.981"))
Container(0.44183067111724206, DateTime("2023-05-03T10:50:09.981"))
⋮
Container(0.21275325287594737, DateTime("2023-05-03T10:50:09.983"))
Container(0.7248760274958712, DateTime("2023-05-03T10:50:09.983"))
julia> function increase_hour(container)
@set container.d = container.d + Hour(1)
end
increase_hour (generic function with 1 method)
julia> function increase_hours!(array)
array .= increase_hour.(array)
end
increase_hours! (generic function with 1 method)
julia> @time increase_hours!(arr) # compile once
0.129312 seconds (126.60 k allocations: 8.550 MiB, 43.15% gc time, 99.95% compilation time)
10000-element Vector{Container}:
Container(0.45518384534376943, DateTime("2023-05-03T11:50:09.981"))
Container(0.44183067111724206, DateTime("2023-05-03T11:50:09.981"))
⋮
Container(0.21275325287594737, DateTime("2023-05-03T11:50:09.983"))
Container(0.7248760274958712, DateTime("2023-05-03T11:50:09.983"))
julia> @time increase_hours!(arr) # tada, no allocation
0.000033 seconds
10000-element Vector{Container}:
Container(0.45518384534376943, DateTime("2023-05-03T12:50:09.981"))
Container(0.44183067111724206, DateTime("2023-05-03T12:50:09.981"))
In addition to performance with immutable structs, Accessors are also very convenient — this is hard to beat even if you allow for mutability. I mean stuff like @set year(container.d) = 2022, involving functions in addition to properties.