Mutate a Dates.Date

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.

Date is not mutable:

julia> using Dates

julia> ismutabletype(Date)
false

The allocations you’re seeing are likely from something other than the Date object itself. Are the structs in your arrays mutable?

1 Like

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.

4 Likes

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"))
2 Likes

Woah. I hadn’t thought of that. Thanks, I’ll take a look!

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.