Components as entities (v2) - #24728
Trashtalk217 wants to merge 53 commits into
Conversation
…ents-as-entities-alt
Performance TestingI did some reasonably extensive performance testing. I ran both the entire ecs benchmark and several of our stress test examples. I've got the following data:
And the three examples I ran through were
ConclusionFrom the measurements I took I've come to the conclusion that this PR mostly keeps performance the same. At worst it degrades slightly (mostly based on the microbenchmarks). The best solution to fix the resource performance issue is likely still going to be #24058 or something like it. However! I still think this PR is worth merging, because
Important NoteWhen review and discussing this PR, I prefer if you mostly kept it anchored to some line of code (even if only vaguely related). This makes it easier to follow particular threads of argument. |
There was a problem hiding this comment.
Looks good! I don't believe the #24102 is necessary for this, and can be left for later.
With this change, it becomes possible to express uniqueness regarding ComponentId collections/iterators as well. There are now likely several sections of code where it would make sense to convert the previous ComponentId collection/iterator types to their EntitySet version. That would make for a good follow-up PR!
One place for which this is especially relevant is DynamicComponentFetch, the current dynamic way to access the Components of an entity disjointly.
This functionality should fall under similar design considerations as #18234, though that PR has gotten stalled.
There was a problem hiding this comment.
This still uses a SparseSet over ComponentIds.
Note however that this is accessed once per table when iterating queries (that is, NOT when the QueryState/Query is constructed, but rather every time it's iterated, and multiple times per iteration), so changing it to a EntityHashMap might regress performance for query iteration.
|
I want to place a brief note with regards to this commit: fb25d44 Here I formally replace a couple of I've replaced them with To figure out how much slower (if at all), I (or someone with a better PC) will have to rerun the benchmarks. EDIT: To re-iterate, this is a solvable problem. Namely, #24102 could make it so the first |
| pub struct ComponentsQueuedRegistrator<'w> { | ||
| components: &'w Components, | ||
| ids: &'w ComponentIds, | ||
| allocator: RemoteAllocator, |
There was a problem hiding this comment.
Couldn't this be &'w EntityAllocator since EntityAllocator::alloc takes &self?
There was a problem hiding this comment.
Should be able to downgrade &'w mut EntityAllocator to a shared reference in ComponentsRegistrator as well.
SkiFire13
left a comment
There was a problem hiding this comment.
This is still a couple of type still using SparseSet<ComponentId>:
RemovedComponentMessagesNonSendsSparseSets
This could however be fine since these are all "one-instance" types, so we would not end up paying the cost for every table or archetype.
| entities: Vec<ArchetypeEntity>, | ||
| components: ImmutableSparseSet<ComponentId, ArchetypeComponentInfo>, | ||
| component_ids: Vec<ComponentId>, | ||
| archetype_components: ComponentIdMap<ArchetypeComponentInfo>, |
There was a problem hiding this comment.
We should be able to remove this field completely, because all it stores is the StorageType for the given ComponentId, which we can fetch from Components as needed.
| edges: Edges, | ||
| entities: Vec<ArchetypeEntity>, | ||
| components: ImmutableSparseSet<ComponentId, ArchetypeComponentInfo>, | ||
| component_ids: Vec<ComponentId>, |
There was a problem hiding this comment.
Box<[ComponentId]>, the component list is static for a given archetype.
Dont use RemoteAllocator in component queued registration
Remove ArchetypeComponentInfo and stop storing it in Archetype
…ents-as-entities-alt
…ents-as-entities-alt
| // - The caller ensures that all new columns will be written to immediately. | ||
| let dst_row = unsafe { dst_table.allocate(src_table.entities.swap_remove(row.index())) }; | ||
|
|
||
| let mut dst_iter = dst_table.columns.iter_mut().peekable(); |
There was a problem hiding this comment.
This looks like it's reverting the gains from #23151. I think you might need a version of ComponentIdMap based on sorted boxed slices so that we can do a merge without needing to hash anything.
There was a problem hiding this comment.
I think you might need a version of
ComponentIdMapbased on sorted boxed slices
This would be trivial with an IndexMap-based ComponentIdMap
There was a problem hiding this comment.
This would be trivial with an
IndexMap-basedComponentIdMap
Yeah, but you'd still need to make sure the components are sorted... and at that point the HashMap part isn't buying you much because you can use binary search for lookups.
I don't know of a good off-the-shelf implementation of a map backed by sorted slices, though.
Or, I guess you could just use a BTreeMap, but we aren't trying to modify it after creation so the extra complexity over a sorted slice doesn't seem worthwhile.
There was a problem hiding this comment.
but you'd still need to make sure the components are sorted...
That's the same as what we have been doing with boxed slices, just with an additional hashmap backing lookups though.
and at that point the HashMap part isn't buying you much because you can use binary search for lookups.
I'm not sure that binary search has the same performance characteristics as a hash lookup.
There was a problem hiding this comment.
That's the same as what we have been doing with boxed slices, just with an additional hashmap backing lookups though.
Yup, exactly. I'm just arguing that the hashmap isn't worth the overhead at that point, and we could drop down to just two boxed slices. We'd have to profile, of course!
I'm not sure that binary search has the same performance characteristics as a hash lookup.
Yeah, it has slightly worse big-O, but log(N) isn't bad even for large N, and it uses less memory and has a better constant factor. For that matter, a lot of archetypes will be small enough that a linear scan might even be fastest!
There was a problem hiding this comment.
Sorry, let me rephrase that: Yup, IndexMap with sorted components is a good idea and we should try it! And then, once that's working, I have some crazy ideas to micro optimize it further that we could test!
There was a problem hiding this comment.
Yeah, it has slightly worse big-O, but log(N) isn't bad even for large N, and it uses less memory and has a better constant factor.
I'm not so sure about the constant factor, as it definitely depends on the cost of the hash function you're using. Binary search also has terrible branch prediction (it's the worst case for it!)
For that matter, a lot of archetypes will be small enough that a linear scan might even be fastest!
Yes, for small arrays linear scans might be better than both binary search and hash tables. In fact rustc even a small map optimization where it uses an arrayvec for small maps and a hashmap for bigger ones, with the threshold being 8 (but it could be tweaked).
See #23988.
Change
The primary difference between this and #23988, is that this is not reliant on Entity Ranges (#24102). Because of this, there is a possibility for a performance regression with regards to
Components.componentsgoing from aSparseArraytoHashMap. There is an additional hash operation. Additionally, the fields onAccesshave changed from aFixedBitSetto aHashSet, so the set operations are likely slower.