Faster way to "save" and "load" structs

I’m working on a project in which I need to repeatedly remember the status of my process, make a small change, and then re-load the original status from before the change was made. Think something like a Monte Carlo tree search where I’m trying to reset to the root node after going down a branch.

Information about my process is stored in a custom struct, so I have something like the following:

mutable struct my_struct
    a::Array{Any,1}
end
function save_or_load(status::my_struct)
    return deepcopy(status)
end

active_state = my_struct([1, 2, [3, 4, [5, 6]]])
saved_state = save_or_load(active_state)

I need whatever process state is saved (“saved_state”) to be unaffected by future changes to the active state (“active_state”), which is why I’ve used deepcopy(). My problem is that this is very slow. From BenchmarkTools, loading a state in my system takes roughly 0.5ms, which makes resetting my system the bottleneck for the whole process since it needs to do this thousands of times.

Something worth noting is that once a state is saved, I should never need to modify the saved status (i.e., “saved_state” doesn’t need to be mutable). It only acts as a reference point for resetting the active state. Although I’m not sure if that information can be leveraged to speed up my saving/loading.

Is there a faster way to do this?

The Any is likely to be the bottleneck here.
If you can, try to split your status struct, like:

struct my_struct
    a::Vector{Int}
    ... other Vectors of concrete types as needed...
end

Thanks for the idea! I checked through my struct and was able to change over all the instances of Any, though the time seems to be unaffected. In the full version my struct is composed of 12 fields with types ranging from Int to Vector{Union{Missing, Custom_Struct_1, Custom_Struct_2}}, which I’m not sure I’m able to specify any further.

Doing this fully generically is hard.
If you really have to do it generically, you might even want to look into forking the process (assuming linux) although that comes with a whole lot of crevates.

Doing this nongenerically is much easier, and can make it much faster.
If you can expres your code as:

change!(state)
f(state)  # doesn't change state
change_state_back!(state)

then it is much cheaper.

For example how FiniteDifferences.jl considers all permutations of each element on the inputs

It just copies the sncle element that it will change x[n] performs the operation, then changes x[n] back.

You can also build a collection of these functions.
Which are probably a form of optic (not that that is useful)
Where each function mutates the input, and returns a function that tranforms it back.
Then you can compose those.

Hm, interesting idea. I can see how doing this nongenerically would be faster, although creating the change_state_back!(state) functions could get very messy. For example, how change!(state) works depends on state. So in that case, change!(state) would need to keep records of how it was implemented so that it could be reversed properly, or at least keep track of the relevant properties of the state it modified.

Probably the cleanest way is, unfortunately, to do it fully generically. But I’ll keep the nongeneric idea in mind in case the slowness effectively kills the project (for now it’s just annoying).