-
-
Notifications
You must be signed in to change notification settings - Fork 62
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
Archetypes #137
Comments
It would actually be a MASSIVE rework :D So currently DefaultEcs relies on a sparse set implementation to store components, each type in their own array. To speed up entity enumeration they need to be cached based on the component composition you are querying (those are the entity containers set, map, ...) or else you would need to check every entities each time you want to enumerate a query. While this add some complexity it also allow to build more feature on top of it like component sharing (I know Unity allow that too but with my implementation it is completely free and require no extra code), predicate on component in the query, reactive container, ... Now from what I understand archetype can be done in two ways. The first one is Array of Structs like your draft The other way is Struct of Arrays The more I type about it the more I am dangerously close of thinking that it may be worth the trouble haha. Maybe I will try to do a POC on a branch and see how bad the api loss would be with this change and if it can be mitigated. |
Thanks for your fast reply and the insight ! :D Looks like thats right, there two different ways as you mentioned. I researched a bit, using an AOS ( array of structs ) I actually thought that using the SOA ( structs of arrays ) approach is a bit more difficult... atleast i dont know how we could realize this properly... generics cant be used here i guess. So doesnt this also involve pointer magic ? Or code generation atleast ^^ But the performance difference should not be that huge, because both are pretty cache efficient... so even if the array of structs approach should be faster, it should only be a bit faster ( Would actually require some benchmarking ). About unity... no one really can tell what exactly they are using under the hood. I asked some "experts" in the different discords and even they arent sure about what unity exactly uses :D I also think its worth the trouble ^^ Furthermore it would really speed up this ecs even further which is always a good thing. So thanks a lot for your time and effort ! -- Update According to this Unity Talk here, unity uses structure of arrays ^^ I think the easiest way to represent SOA for the archetypes is using pointers. This might be unsafe, but its easier than code generation. After some brainstorming, it might could look like this ( just found out that theres a system.array type which could help us ) // An archetype, basically all entities with the same components.
public unsafen class Archetype {
// An id which represents a bunch of types, somehow generated from a Type[] array basically
public int id;
public Type[] types;
// The entities within that archetype
public Entity[] entities;
public System.Array[] componentArrays; // A array which stores a bunch of different arrays e.g. ((Vector3[])componentArrays[0])[0]
public int size;
} |
Oh generic can be completely abused! That's actually what DefaultEcs is already doing to remove any cast/reflection when setting/getting components. It uses static generic type to hold worlds data, basically each world has its own internal id that is used to access a specific instance of component pool in a static generic array. This remove the need for casting and is much faster than a dictionary access that would be traditionally used, we use the type system as our dictionary. There may be some memory overhead but it is far outweighed by the benefits. So if we give our archetype their own id, we can now give each of them their own component pool with the bonus that when we need to enumerate their entities, every component access will be direct What we currently have public ref T Get<T>() => ref ComponentManager<T>.Pools[WorldId].Get(EntityId); What it could look like public ref T Get<T>() => ref ComponentManager<T>.Pools[World.EntityInfos[EntityId].ArchetypeId].Get(EntityId); An out of context Now all that is good by I just remembered the feature that would be hard to port with archetypes: enable/disable entities and component individually. It's probably possible to replicate by duplicating archetype with the same composition but pools that are not part of the queryable data. I definitely need to look into it. |
Thats actually cool :D I didnt knew this. If you want to disable a whole instance, you can swap it to the end of the active instances and decrement your active count, and then only iterate over the first activeCount entries of each array. That keeps the iteration dense. But requires some more effort to move it to the end. For disabling/enabling components... flags are mostly used. Either directly inside the component ( Wrapper for example ) or by using another lookup. Another option is to "lend" the entity to another archetype that has only the active components on it. The original archetype's copy stays around - swapped to the inactive pile as described above - so you don't lose the data stored on the inactive components. When the component is re-enabled, copy the entity's other data back from the archetype you lent it to, and re-activate it in this archetype. We probably mean the same here with those pools :D If our solutions arent enough, i bet there still many others out there ^^ |
we can't do that because if the entity change of archetype and you stored it on your own, you won't get the updated archetype id because it is a struct :(
If all the entity data need to be copied anyway, maybe it is still simpler to think about it as a new archetype. The only big optimization is when by chance you disable the last entity of the archetype: nothing to copy then but it probably won't happen a lot. |
Well thats correct :/ i havent thought about that. So we need a lookup at that palce... but i guess its actually just an array operation, right ? If thats the case it would still be very fast. And yes thats true, the copy operation ( and algo, because you need additional work to swap it ) are probably to much and there cleaner ways of doing that :D Im excited how your test turns out and how it will look like. |
Although i like the idea of raw increase in speed on iteration through entities when you only getting components, please keep in mind that is not always possible or convenient. Things like Set/Remove or Enable/Disable is already have their share of limitations (threading, for example) and it would be nice to not increase that list. |
There should be no further limitations, add/set operations should behave pretty normal and shouldnt decrease in performance that much ( It needs to copy the entity over into a new archetype but thats all, everything has pros and cons... and disabling/enabling will probably being used more often ). The only part ( right now ) which needs some rework is the disabling/enabling of components, which we will find a good solution for. The new architecture also should have no constraints in terms of threading. Atleast i cant imagine one. I assume the entity recorder will also work fine ( just needs to change some code under the hood ). |
Those limitations will stay the same but the new access will also comes with its own caveats. When accessing components you will probably get something like a Anyway I started experimenting a little. For now I should be able to keep the existing entity api intact (except for |
That actually sounds great ! :D I bet you will find a good solution for making those runtime transistions fast. Thanks a lot for all your effort and time ! ^^ Excited to see the outcome. |
Ok so I have something working. There is still a lot of work to do but here are some rough numbers:
things to note:
api that may be lost:
Obviously this is just the beginning and for now I still consider it a POC, I need to work on the query for archetypes and see where it goes. |
Those results are actually looking pretty good :D How many entities were processed ? I actually spend some time debugging the unity ecs source code... Their Their archetype is split into chunks and each Component-Type has its own id. At some point they actually use an array to map the
(Yeah... they really loop over all components inside the chunk)
Again, thanks for your time & effort ! :D |
Will new way of access components be as convenient as Get, then? For example, i often request components from other entities, or request components that are not part of system entity filter(after Has call, ofc). |
100000, benchmarks adapted from here
I didn't split it into chunk because one array per component per archetype is already complicated enough but I also have a entity id to archetype index mapping to access an entity data randomly through the Get/Set. The big performance loss of those method is because I handle enable/disable state for each component type :/ this cause an indirection on the actual component pool I need to access (is it the archetype pool with the mapping of the archetype or the shared pool for disabled component with its own mapping). The goal of this feature was to speed up composition change because no data needed to be moved but because of archetype this is no longer true so it doesn't really give any speed up compared to just removing the component from the entity and adding it back later. The only benefit is keeping the value of the component for you to later add it back :/ I am not sure if I just drop it (but keep the enable/disable at the entity level) or find an other way to do it.
The goal is to try to make as little as possible breaking changes, old codes should works as before with maybe a slight performance loss until you switch to the new stuff with all the benefit. Futur will tell if this holds true ^^"
Will there be extension methods to wrap all this stuff for you or exclusively code generation through the DefaultEcs.Analyzer package I still don't know, I really don't want to maintain stuff like this or this x) |
Wouldnt that disable/enable mechanic still be way faster than triggering an remove/add operation which would copy the entity into a different archetype ? ^^ Atleast thats what i always hear... furthermore the state gets lost during that operation. My 2 cents... I could imagine four different ways to implement disabling/enabling in an archetype way.
The last way was often recommended by people all over the different discords im in ^^ |
The way I currently handle it is like this:
when enumerating you would have to check each entity individually for what is activated or not, you would lose the benefit of no branch path when iterating on a archetype entities. The
not a fan of this as you can't enable/disable any component type that could come from an external library, you would have to create a wrapper (your last point) for them, and the previous problem still remains :/ Some frameworks like bevy allow you to chose between table storage (archetype) or sparse set for each component, maybe this is something that could be used to reallow shared component with minimal effort. |
This would be causing boxing if your component is a struct and you work with it after casting your type to interface. |
Oh guess i forget a way ^^ This also looks fine. |
If you do not use the built-in enable/disable feature and do your own logic for it through
So in theory if you would set all your component types to the Shared mode (maybe a parameter of the Maybe later I will try to make the storage mode change as needed so for example by default you have Archetype but the first time you do a
Anything else would throw. |
It is starting to look great, when using Single or Shared mode for component, the overhead is ~x2 compared to current implementation, all api should stay intact. In archetype mode the overhead is ~x4 when using the old A lot of work still remains, the next big question is what to do with MultiMap systems, is it possible to do something so they can benefit from archetypes? A new api should be also created for entities to batch modifications together so the archetype is only changed once even if we do multiple Set/Remove to limit copy. |
That actually sounds insane ! ^^ Keep up the great work and its great to hear such good news. |
I hate to jump into this discussion especially since I've just skimmed what has been covered already. Unity have a patent about architypes and their implementation in a single array, other people with ECS implementations have been discussing this and how it affects them. Bevy for example have a sparse array implementation that wouldn't be affected (as it doesn't use archetypes)...
I just wanted to make sure people are aware before someone gets stung really badly by this even though I would love to see speed improvements. |
Thanks for the links, I have already seen it around the same time as the reddit thread started. Keep in mind that the change to Archetypes for DefaultEcs is still far away but the implementation I went with is probably closer to Bevy one: components are stored in their own sparse array but each "archetype" has its own sets of component so an entity components all share the same index for access, probably not as fast as Unity solution but from my first benchmark it still gives a noticeable boost! Archetype or not at least it should be safe from this horrible patent... |
Soooo lets talk about archetypes.
If i am correct, archetypes offer a higher iteration speed. This is because entities with the same components are stored in one big array. Which means that the iteration over them can be loaded more efficient into the cpu. So iteration is a lot more cache friendly, which results in a performance boost.
So we should probably look into this. I assume that this wouldnt even require a major rework. Only the component storage would need some optimization.
I recently looked into those articles.
Theory behind archetypes
Unitys ECS
C# Archetype ECS
So a very quick and dirty first draft of a archetype based component storage would look like this...
A few problems remain...
You know the source code of default.ECS way better than i do, would you mean that such an archetype based architecture would improve the speed and how hard would it be to change the underlaying component storage ?
The text was updated successfully, but these errors were encountered: