Skip to content

Conversation

@mjhostet
Copy link
Contributor

TL;DR: this cuts in half the number of atomic ops per
Entities::reserve_entity and also cuts in half the memory
overhead related to Entity freelisting and allocation.

Entities maintained two tables: reserved and free, each
with one slot for every possible Entity. This way every entity could
be freelisted or reserved without needing more memory. Combined,
these tables took 2*meta.len() slots.

The key observation is that any entity can only be in one table or the
other, but not both, so we actually only need meta.size() slots total,
i.e. half the memory. One way to take advantage of this would be to
have the freelist grow "up" from the bottom of a Vec, and reserved
grow "down" from the top of the same Vec, because we know they
can't meet in the middle. But there is a better way.

This change combines free and reserved into unused, cutting the
memory in half, and also lays them out like this:

------------------------------------------
| freelist | reserved | <room to expand> |
------------------------------------------
           ^          ^
  freelist_cursor   reserved_cursor

When reserve_entity wants to move something from the freelist
to the reserved list, it simply atomically decrements freelist_cursor,
the boundary between the two, without moving any array entries.
This avoids the previous atomic operation to update reserved.

reserved_cursor no longer needs to be atomic because it is only
modified by &mut methods.

This approach does make alloc() and free() slightly more complicated.
alloc() has to update reserved_cursor to match freelist_cursor
(just a single store). This works if you can't have unflushed data
when you alloc, which is my understanding.

If free tries to push an item and there are reserved values, it
needs to move the first reserved value to the end of the reserved
range to make room for the larger freelist. I don't know if this can
happen -- is it actually legal to free into an unflushed Entities?
If not, then we don't need some of this code.

This change also has a few other tweaks:

  • When grow() copies values into the freelist, put them "before"
    the old freelist values (if any), so they get popped last. This way
    we are more likely to hand out lower IDs, only allocating higher IDs
    when we really need them.

  • Added some unit tests.

I wasn't completely clear on the reserved_len (etc.) code and its
requirements (what is legal while iterating with those?), so I took my
best guess how to update it.

TL;DR: this cuts in half the number of atomic ops per
`Entities::reserve_entity` and also cuts in half the memory
overhead related to Entity freelisting and allocation.

Entities maintained two tables: `reserved` and `free`, each
with one slot for every possible Entity. This way every entity could
be freelisted or reserved without needing more memory. Combined,
these tables took 2*meta.len() slots.

The key observation is that any entity can only be in one table or the
other, but not both, so we actually only need meta.size() slots total,
i.e. half the memory. One way to take advantage of this would be to
have the freelist grow "up" from the bottom of a Vec, and reserved
grow "down" from the top of the same Vec, because we know they
can't meet in the middle.  But there is a better way.

This change combines `free` and `reserved` into `unused`, cutting the
memory in half, and also lays them out like this:

```
------------------------------------------
| freelist | reserved | <room to expand> |
------------------------------------------
           ^          ^
  freelist_cursor   reserved_cursor
```

When `reserve_entity` wants to move something from the freelist
to the reserved list, it simply atomically decrements `freelist_cursor`,
the boundary between the two, without moving any array entries.
This avoids the previous atomic operation to update `reserved`.

`reserved_cursor` no longer needs to be atomic because it is only
modified by `&mut` methods.

This approach does make `alloc()` and `free()` slightly more complicated.
`alloc()` has to update `reserved_cursor` to match `freelist_cursor`
(just a single store). This works if you can't have unflushed data
when you alloc, which is my understanding.

If `free` tries to push an item and there are reserved values, it
needs to move the first reserved value to the end of the reserved
range to make room for the larger freelist. I don't know if this can
happen -- is it actually legal to free into an unflushed Entities?
If not, then we don't need some of this code.

This change also has a few other tweaks:

- When grow() dumps copies values into the freelist, put them "before"
  the old freelist values (if any), so they get popped last. This way
  we are more likely to hand out lower IDs, only allocating higher IDs
  when we really need them.

- Added some unit tests.

I wasn't completely clear on the `reserved_len` (etc.) code and its
requirements (what is legal while iterating with those?), so I took my
best guess how to update it.
@memoryruins memoryruins added C-Code-Quality A section of code that is hard to understand or change A-ECS Entities, components, systems, and events labels Oct 11, 2020
@cart
Copy link
Member

cart commented Oct 11, 2020

I do love optimizations :)

Given that this touches hecs code, it might also be a good idea to submit a PR to the hecs repo so they can get these optimizations too. @Ralith is this something you're interested in? It would be nice to get your feedback on this too given that you wrote entities.rs.

@mjhostet
Copy link
Contributor Author

Another idea: if it's not legal to free() while unflushed stuff exists (unclear to me), the pending atomic could be deleted, as well as the free_cursor CAS. Instead, just use simpler code that unconditionally decrements free_cursor, and the more negative it goes, the more pending values there are.

@mjhostet
Copy link
Contributor Author

Another thing that becomes possible by not moving freelist values around is that it's easy to add a reserve_entities method that returns N entities at once. It would return an iterator that contains both a slice pointing into the Entities freelist, and a numeric range of "pending" IDs consumed if the freelist ran out. Combined with removing the pending atomic by letting free_cursor (now AtomicI64) go negative, that means that it would only take a single atomic subtract to reserve any number of IDs, and allocate no memory.

@Ralith
Copy link

Ralith commented Oct 11, 2020

These changes (subject to detailed review) would be very welcome in hecs!

is it actually legal to free into an unflushed Entities?

I'm not certain what you mean by "into" here. free is not currently supported on reserved, unflushed entities, but may in principle be called prior to flush on real entities. At least in upstream hecs, we only ever call free immediately after flush (in World::despawn), so I'd be perfectly happy to strengthen that requirement to "must only be called with no pending (i.e. unflushed) entities" if it's useful.

@mjhostet
Copy link
Contributor Author

Thanks @Ralith, that's the strengthened requirement I was interested in. I'll put together a PR for hecs and we can talk more about it there.

@mjhostet
Copy link
Contributor Author

I will submit something upstream, so withdrawing this PR.

@mjhostet mjhostet closed this Oct 11, 2020
@cart
Copy link
Member

cart commented Oct 11, 2020

Cool cool. If/when this gets merged there, feel free to open up another pr here.

@mjhostet
Copy link
Contributor Author

Upstream PR is Ralith/hecs#86

@mjhostet
Copy link
Contributor Author

@cart @Ralith please let me know if there is a specific ECS benchmark you'd like me to make faster.

@Ralith
Copy link

Ralith commented Oct 17, 2020

The first thing that comes to my mind perfwise is the inlining improvements that were made in bevy which haven't yet been upstreamed. Help getting that synced back up would be great!

@cart
Copy link
Member

cart commented Oct 17, 2020

Yeah I think the inlining and "Entity as query" improvements are clear / relatively easy things to upstream. Sorry @Ralith for not driving those upstream myself yet. @mjhostet you are definitely welcome to do that if you want.

github-merge-queue bot pushed a commit that referenced this pull request Mar 10, 2025
Updates the requirements on
[petgraph](https://github.com/petgraph/petgraph) to permit the latest
version.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/petgraph/petgraph/blob/master/RELEASES.rst">petgraph's
changelog</a>.</em></p>
<blockquote>
<h1>Version 0.7.1 (2025-01-08)</h1>
<ul>
<li>Do not unnecessarily restrict <code>indexmap</code> version
(<code>[#714](https://github.com/petgraph/petgraph/issues/714)</code>_)</li>
<li>Export <code>UndirectedAdaptor</code>
(<code>[#717](https://github.com/petgraph/petgraph/issues/717)</code>_)</li>
</ul>
<p>..
_<code>[#714](https://github.com/petgraph/petgraph/issues/714)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/714">petgraph/petgraph#714</a>
..
_<code>[#717](https://github.com/petgraph/petgraph/issues/717)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/717">petgraph/petgraph#717</a></p>
<h1>Version 0.7.0 (2024-12-31)</h1>
<ul>
<li>Re-released version 0.6.6 with the correct version number, as it
included a major update to an exposed crate
(<code>[#664](https://github.com/petgraph/petgraph/issues/664)</code>_).</li>
</ul>
<h1>Version 0.6.6 (2024-12-31 - yanked)</h1>
<ul>
<li>Add graph6 format encoder and decoder
(<code>[#658](https://github.com/petgraph/petgraph/issues/658)</code>_)</li>
<li>Dynamic Topological Sort algorithm support
(<code>[#675](https://github.com/petgraph/petgraph/issues/675)</code>_)</li>
<li>Add <code>UndirectedAdaptor</code>
(<code>[#695](https://github.com/petgraph/petgraph/issues/695)</code>_)</li>
<li>Add <code>LowerHex</code> and <code>UpperHex</code> implementations
for <code>Dot</code>
(<code>[#687](https://github.com/petgraph/petgraph/issues/687)</code>_)</li>
<li>Make <code>serde</code> support more complete
(<code>[#550](https://github.com/petgraph/petgraph/issues/550)</code>_)</li>
<li>Process multiple edges in the Floyd-Warshall implementation
(<code>[#685](https://github.com/petgraph/petgraph/issues/685)</code>_)</li>
<li>Update <code>fixedbitset</code> to 0.5.7
(<code>[#664](https://github.com/petgraph/petgraph/issues/664)</code>_)</li>
<li>Fix <code>immediately_dominated_by</code> function called on root of
graph returns root itself
(<code>[#670](https://github.com/petgraph/petgraph/issues/670)</code>_)</li>
<li>Fix adjacency matrix for <code>Csr</code> and <code>List</code>
(<code>[#648](https://github.com/petgraph/petgraph/issues/648)</code>_)</li>
<li>Fix clippy warnings
(<code>[#701](https://github.com/petgraph/petgraph/issues/701)</code>_)</li>
<li>Add performance note to the <code>all_simple_paths</code> function
documentation
(<code>[#693](https://github.com/petgraph/petgraph/issues/693)</code>_)</li>
</ul>
<p>..
_<code>[#658](https://github.com/petgraph/petgraph/issues/658)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/658">petgraph/petgraph#658</a>
..
_<code>[#675](https://github.com/petgraph/petgraph/issues/675)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/675">petgraph/petgraph#675</a>
..
_<code>[#695](https://github.com/petgraph/petgraph/issues/695)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/695">petgraph/petgraph#695</a>
..
_<code>[#687](https://github.com/petgraph/petgraph/issues/687)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/687">petgraph/petgraph#687</a>
..
_<code>[#550](https://github.com/petgraph/petgraph/issues/550)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/550">petgraph/petgraph#550</a>
..
_<code>[#685](https://github.com/petgraph/petgraph/issues/685)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/685">petgraph/petgraph#685</a>
..
_<code>[#664](https://github.com/petgraph/petgraph/issues/664)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/664">petgraph/petgraph#664</a>
..
_<code>[#670](https://github.com/petgraph/petgraph/issues/670)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/670">petgraph/petgraph#670</a>
..
_<code>[#648](https://github.com/petgraph/petgraph/issues/648)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/648">petgraph/petgraph#648</a>
..
_<code>[#701](https://github.com/petgraph/petgraph/issues/701)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/701">petgraph/petgraph#701</a>
..
_<code>[#693](https://github.com/petgraph/petgraph/issues/693)</code>:
<a
href="https://redirect.github.com/petgraph/petgraph/pull/693">petgraph/petgraph#693</a></p>
<h1>Version 0.6.5 (2024-05-06)</h1>
<ul>
<li>Add rayon support for <code>GraphMap</code>
(<code>[#573](https://github.com/petgraph/petgraph/issues/573)</code><em>,
<code>[#615](https://github.com/petgraph/petgraph/issues/615)</code></em>)</li>
<li>Add <code>Topo::with_initials</code> method
(<code>[#585](https://github.com/petgraph/petgraph/issues/585)</code>_)</li>
<li>Add logo to the project
(<code>[#598](https://github.com/petgraph/petgraph/issues/598)</code>_)</li>
<li>Add Ford-Fulkerson algorithm
(<code>[#640](https://github.com/petgraph/petgraph/issues/640)</code>_)</li>
<li>Update <code>itertools</code> to 0.12.1
(<code>[#628](https://github.com/petgraph/petgraph/issues/628)</code>_)</li>
<li>Update <code>GraphMap</code> to allow custom hash functions
(<code>[#622](https://github.com/petgraph/petgraph/issues/622)</code>_)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/petgraph/petgraph/commit/2765d2a55044a35a20d95c50a2f318fbc3bb85f4"><code>2765d2a</code></a>
Release 0.7.1 (<a
href="https://redirect.github.com/petgraph/petgraph/issues/722">#722</a>)</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/d341db9d18582cfcffebf320896947e55ecba09c"><code>d341db9</code></a>
ci: downgrade hashbrown rather than limiting indexmap (<a
href="https://redirect.github.com/petgraph/petgraph/issues/714">#714</a>)</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/73c64b629f38dc8b0a8edc7550f16a662ed05c25"><code>73c64b6</code></a>
Make UndirectedAdaptor &amp; inner G pub (<a
href="https://redirect.github.com/petgraph/petgraph/issues/717">#717</a>)</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/d057429081bc02812d3605d1e7159f0e73361e51"><code>d057429</code></a>
Release <code>0.7.0</code> (<a
href="https://redirect.github.com/petgraph/petgraph/issues/713">#713</a>)</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/13ebd7d2ddd4a1ac07a33606c0d4f82d342e5fa6"><code>13ebd7d</code></a>
Release <code>0.6.6</code> (<a
href="https://redirect.github.com/petgraph/petgraph/issues/706">#706</a>)</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/159341e4af2a1292d8a1da428d64784e2dfc8ae5"><code>159341e</code></a>
Implement DSatur graph coloring algorithm</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/7fa3aac97168de7fca54644a5b45c464d5245535"><code>7fa3aac</code></a>
fix: adjacency matrix for csr and adjacency list (<a
href="https://redirect.github.com/petgraph/petgraph/issues/648">#648</a>)</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/9fda6bbe2e52663d03317083d2623faa4f0d4cd4"><code>9fda6bb</code></a>
Update gitignore with possible editor extensions to ensure they do not
occur ...</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/9b5837e395c342e9cb2e5a2b3870fa7ee6650ef4"><code>9b5837e</code></a>
Allow clippy::needless_range_loop in benches</li>
<li><a
href="https://github.com/petgraph/petgraph/commit/ad9f83c2ae237ba6f5832a19f01ef8c3ae14dd19"><code>ad9f83c</code></a>
Process warnings in 'test' target</li>
<li>Additional commits viewable in <a
href="https://github.com/petgraph/petgraph/compare/[email protected]@v0.7.1">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-ECS Entities, components, systems, and events C-Code-Quality A section of code that is hard to understand or change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants