Skip to content

perf: cache the matching archetypes per query#33

Merged
ragoune merged 1 commit into
masterfrom
perf/query-archetype-cache
Jun 28, 2026
Merged

perf: cache the matching archetypes per query#33
ragoune merged 1 commit into
masterfrom
perf/query-archetype-cache

Conversation

@ragoune

@ragoune ragoune commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Every Foreach/Task/Count/GetEntitiesIds rebuilt the query's filter id list and rescanned all archetypes (O(archetypes)), allocating and copying the matched archetype structs on every call.

Each query now holds a shared filterCache: the filter ids (required components + tags) are computed once at creation, and the matching archetype ids are memoized in a reused buffer, recomputed only when a new archetype appears — detected via len(world.archetypes) as a monotonic version, since archetypes are never removed. Query methods iterate the cached ids and read world.archetypes[id] live, so entities moving between existing archetypes are still seen.

A reused query over a world with many archetypes now costs the same regardless of archetype count (O(archetypes) -> O(1)) and allocates nothing on the read path. getArchetypesForComponentsIds is removed (replaced by World.matchArchetypes).

Measured on go-ecs-benchmarks (Ryzen 5800X), query reused across calls:

  • query256arch: -88% (N=1), -97% (N=64), -90% (N=256) vs v1.7.0
  • query32arch: -87% to -90% at low/mid N
  • random (mixed load): -40% across N

Every Foreach/Task/Count/GetEntitiesIds rebuilt the query's filter id list and rescanned all archetypes (O(archetypes)), allocating and copying the matched archetype structs on every call.

Each query now holds a shared filterCache: the filter ids (required components + tags) are computed once at creation, and the matching archetype ids are memoized in a reused buffer, recomputed only when a new archetype appears — detected via len(world.archetypes) as a monotonic version, since archetypes are never removed. Query methods iterate the cached ids and read world.archetypes[id] live, so entities moving between existing archetypes are still seen.

A reused query over a world with many archetypes now costs the same regardless of archetype count (O(archetypes) -> O(1)) and allocates nothing on the read path. getArchetypesForComponentsIds is removed (replaced by World.matchArchetypes).

Measured on go-ecs-benchmarks (Ryzen 5800X), query reused across calls:
- query256arch: -88% (N=1), -97% (N=64), -90% (N=256) vs v1.7.0
- query32arch:  -87% to -90% at low/mid N
- random (mixed load): -40% across N
@ragoune ragoune merged commit 3ed1591 into master Jun 28, 2026
4 checks passed
@ragoune ragoune deleted the perf/query-archetype-cache branch June 28, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant