-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Some thoughts on Overseer usage and design #6
Comments
I started convertingt Simons playground to use Overseer, though it's very work-in-progress and probably has a bunch of bad decisions in it. It's also not yet at a point where it's usable, but maybe it's good to mention it anyway. In case we want to coordinate things. https://github.com/ffreyer/MakieCore.jl/tree/ECS |
That'd need some reference to the ledger, right? I do like the
I don't really like the name
Functions in an array sound like a bad thing for performance to me, and a quick test seems to agree with that. Functors seem to be pretty much the same as System structs if they inherit from System, but if they are about as slow as functions if they don't. In my mind this library wants the label "high performance" so I'm leaning towards the current way of doing things. using BenchmarkTools
abstract type System end
struct F1 <: System end
struct F2 <: System end
struct F3 <: System end
(::F1)(data) = data .= sin.(data)
(::F2)(data) = data .= exp.(data)
(::F3)(data) = data .= sqrt.(data)
function execute(fs::Vector{System}, data)
for f in fs
execute(f, data)
end
end
function execute2(fs::Vector{System}, data)
for f in fs
f(data)
end
end
f1(data) = data .= sin.(data)
f2(data) = data .= exp.(data)
f3(data) = data .= sqrt.(data)
function execute(fs::Vector{Function}, data)
for f in fs
f(data)
end
end
functions = [f1, f2, f1, f3, f2]
systems = [F1(), F2(), F1(), F3(), F2()]
functors = [G1(), G2(), G1(), G3(), G2()]
data = [1.0, 2.0, 3.0]
@benchmark execute($systems, $data)
# BenchmarkTools.Trial:
# memory estimate: 0 bytes
# allocs estimate: 0
# --------------
# minimum time: 91.713 ns (0.00% GC)
# median time: 94.320 ns (0.00% GC)
# mean time: 98.574 ns (0.00% GC)
# maximum time: 339.984 ns (0.00% GC)
# --------------
# samples: 10000
# evals/sample: 952
@benchmark execute2($functors, $data)
# BenchmarkTools.Trial:
# memory estimate: 0 bytes
# allocs estimate: 0
# --------------
# minimum time: 93.431 ns (0.00% GC)
# median time: 95.975 ns (0.00% GC)
# mean time: 100.941 ns (0.00% GC)
# maximum time: 355.987 ns (0.00% GC)
# --------------
# samples: 10000
# evals/sample: 950
@benchmark execute($functions, $data)
# BenchmarkTools.Trial:
# memory estimate: 0 bytes
# allocs estimate: 0
# --------------
# minimum time: 131.536 ns (0.00% GC)
# median time: 136.016 ns (0.00% GC)
# mean time: 142.128 ns (0.00% GC)
# maximum time: 444.328 ns (0.00% GC)
# --------------
# samples: 10000
# evals/sample: 886 |
Thanks for pointing out the missing ledger argument in my example. I've added that in.
Well.. my claim here is that ledger is fundamentally table-like: entities are row indices, and component types act as column names. Adding a component to an entity affects the sparsity structure. Conceptually it's pretty "normal", the complexity comes from the extra API needed to cover sparse access. Anyway,
This is likely due to the union splitting optimization. I'll guess this difference goes away once you have more than a few systems (maybe five or so?) In general,
Agreed on high performance goals, but I'd point out that looping over systems is the outermost loop in an ECS environment so performance of this particular loop is likely irrelevant. Conversely, the innermost loop is where performance matters. For ECS, the innermost loop is entity iteration and component access inside the entity iteration loop. That part really needs to be as fast as possible. |
I just to point out the connections to the functions in a struct question in https://discourse.julialang.org/t/simple-rule-engine/59125/10 and https://discourse.julialang.org/t/event-handler/59937/5, which was workable for me (I wrote about my context here https://github.com/mschauer/ZigZagBoomerang.jl/wiki/Internals) |
Something I often end up doing is thin wrappers around types, for example A better example of that might be Perhaps saving components as a |
Hi Y'all, First of all, thanks for the interest and useful feedback! I'll try to address most points raised here, I will also open some Issues to discuss specific ones in more detail to keep things clear. Comments/Replies
So far not really, it is actually a remnant from the previous way of handling/accessing components in a ledger.
I'm not entirely sure I support a change like this. I'm not sure about this, but to me it feels that more type instability, compile time and whatnot would be attached to allowing any function type. At least a system is a clear one supertype, with no parametric types and whatnot. Parametrized
I fully support this idea, I like it a lot. In the past I recall trying something similar using
So, to some extent I agree with the Table argument, the reason why I chose something a bit more abstract is that a ledger is more than just the PerformanceIndeed to me performance was always the very first thing I tried to optimize for, and then added syntactic sugar where it wouldn't impact performance significantly, and where going to a more verbose but higher performance syntax remained a plug in replacement. E.g. there is still the possibility of using I would like to keep designing the package in this way, that going to a more verbose performant way does not require a big change in the code. TODOSince there seems to be some eyes on the package now, and interest, I have made a TODO list / list of goals in #9 that I will add to in the future, so I can get some input on the design of those features etc. Cheers! |
Yes, implicit unwrapping was something I had in my examples above, but I guess I didn't explicitly mention it (fixed now). I think the trick is to implicitly unwrap components into their fields so that the user directly works with the types of the fields rather than a wrapper type. (Wrapper types tend to be quite cumbersome to implement and generally don't play well with Julia's multiple dispatch.) There's a few challenges for implicit unwrapping:
|
I guess I'm quite skeptical this makes any practical performance difference, as the compiler will treat containers of both
Yes I think this is true. It just seemed to me that the syntactic overhead of defining new systems was kind of unfortunate and I couldn't really see the value it's bringing.
Perfect approach, I agree 100% :-) |
I didn't mean unwrapping with that, I meant literally saving a |
Hey @louisponet, a while back I said I'd write something about my experience using Overseer. With the discussion about trying it out for Makie (or other "serious" projects?) I thought it would be useful to write down some thoughts I had about the design and ways I'd like to be able to use Overseer.
To summarize my experience so far, I'd say "I love the semantics, but don't entirely love the syntax". Or to expand
I had two ideas about how I feel writing systems could be nicer. Here's a system from my game:
Making systems easier to define
Perhaps this is wishful thinking, but it would be nice if I could just use normal functions for systems. Instead we've got the following stuff to write for each system:
A few thoughts here:
requested_componets
fundamental functionality that we need?System
and just implement this asfunction update_lifetimes(ledger)
statefulparameterized systems that wayrequested_components
was optional we could still dorequested_components(::typeof(update_lifetimes)) = ...
Simpler entity iteration syntax
When writing systems, there's constantly some syntactic overhead in setting up component access. Considering the
LifetimeUpdate
example above, I'd perhaps like to write this in more lightweight fashion asHere the iterator needs to carry enough type information to make things like
e.time
efficient, but I think this is possible withgetproperty
overloading. (Here I'm assuming thate.time
ande.max_age
refer to the names of the fields of TimerComp and LifetimeComp. That is, there's some implicit unwrapping going on.)Notice that this makes certain syntax work on an entity iterator in exactly the same way as it does on an
Array{SomeStruct}
when the struct field names match with the component field names. I think this is a nice usability goal to the extent that it makes sense for ECS. It's the kind of thing achieved by packages such as https://github.com/JuliaArrays/StructsOfArrays.jl which present a syntactic facade on top of an optimized data layout. In the example, suppose we wanted to find the number of expired objects:Other random musings
I found the term
ledger
kind of unintuitive; in some ways I'd prefer to usetable
for this, because the ledger is really just a sparse tabular layout. I feel likeMatrix
andSparseMatrix
are related in a similar way asDataFrame
andLedger
.The text was updated successfully, but these errors were encountered: