Closed Tortar closed 6 months ago
This is great. This is truly great. The only thing we need to make sure is that add_agent!
plays well with it and works as expected, so that
add_agent!(Wolf, only_wolf_fields...)
does what it should and initializes an Animal with correct id, pos, and type
.
SHould we add this option for v6, or you want to do it later? The benefit of doing it at v6 would be that this rather important perforamnce feature would be included in the v6 announcement post.
You are right, this doesn't totally resolve the multi-agent "problem". But it gives a very important option of having only one agent type. I think in the majority of use cases this option would have performance gains.
It would be cool to have this in v6 I agree, but it needs some more work, so I don't promise to finish it up in the very near future :D
This looks great. Haven't tried to clone the branch for testing, but if Wolf
and Hawk
are not defined as types, does this mean that instead of dispatching as f(::Wolf)
and f(::Hawk)
one would always need to check against Animal.type
following a structure of the form f(a::Animal) = a.type == :hawk ? f_hawk(a) : f_wolf(a)
?
I wonder, if it would make sense to dispatch on the value of type
? E.g. f(::Val{:hawk})
, f(::Val{:wolf})
... I'm a bit clueless here, would this have negative performance impacts?
I know you like macros @mastrof :D
I think that it is an interesting idea, it will costs more because this is dynamic dispatch vs branching and branching wins, but in reality the real perf benefit here is to avoid abstract containers, so it will not change much adding that for only agent_step!, I will try it out on the benchmark I re-setuped for testing the macro on the PR.
tried:
Seems fine to me
branching:
561.657 ms (23076750 allocations: 1001.82 MiB)
with Val
589.242 ms (23076750 allocations: 1001.82 MiB)
(Referring to branching_faster_than_dispatch.jl
file)
Mod Version:
function agent_step!(agent::GridAgentAll, model2)
agent_step!(agent, model2, Val(agent.type))
end
agent_step!(agent, model2, ::Val{:gridagentone}) = randomwalk!(agent, model2)
function agent_step!(agent, model2, ::Val{:gridagenttwo})
agent.one += rand(abmrng(model2))
agent.two = rand(abmrng(model2), Bool)
end
function agent_step!(agent, model2, ::Val{:gridagentthree})
if any(a-> a.type == :gridagenttwo, nearby_agents(agent, model2))
agent.two = true
randomwalk!(agent, model2)
end
end
function agent_step!(agent, model2, ::Val{:gridagentfour})
agent.one += sum(a.one for a in nearby_agents(agent, model2))
end
function agent_step!(agent, model2, ::Val{:gridagentfive})
targets = filter!(a->a.one > 1.0, collect(nearby_agents(agent, model2, 3)))
if !isempty(targets)
idx = argmax(map(t->euclidean_distance(agent, t, model2), targets))
farthest = targets[idx]
walk!(agent, sign.(farthest.pos .- agent.pos), model2)
end
end
function agent_step!(agent, model2, ::Val{:gridagentsix})
agent.eight += sum(rand(abmrng(model2), (0, 1)) for a in nearby_agents(agent, model2))
end
But if you do something like this inside the step itself I expect it will have a noticeable impact on performance
Actually I created a package which does more than what it is described here and it could allow memory efficiency to approach the one of multiple types in many cases! It is at https://github.com/Tortar/MixedStructTypes.jl if you want to take a look
I though quite a bit on how to compactify agents when you want only one agent type to avoid abstract containers and dynamic dispatch, and I finally found a satisfying solution in my opinion.
This is the proposed syntax for the macro:
this is the code it would generate:
Generating this syntax from the initial code is a bit hard but it is clearly possible, indeed the code above already works e.g.
@lazy
comes fromLazilyInitializedFields.jl
which in my experiments (and their benchmarks) doesn't incur almost any overhead.Obviously this involves the memory trade-off of having more fields than necessary, so this is just an improved alternative for multi type models, not a substitution.
Notice though some cool features of this implementation:
undef
, which means that for a vector field it takes 1/5 of the memorylazy
macro helps ensuring that the uninitialized fields can't be accessed erroneously because it gives an error if the user does it.cc @Datseris because I know you thought a lot about this too, so I want that you see this concrete proposal :-)