Automatic Cache Invalidation vs. Manual Query Refresh: Which Approach Yields Better Performance?
0 reputation · 29 May 2025, 00:07 UTC
0 reputation · 29 May 2025, 00:07 UTC
When a Bevy component changes, the engine’s query cache can either be invalidated automatically or left untouched, forcing the query to recompute on the next frame. The automatic strategy guarantees that queries always see the latest data, but may incur unnecessary recomputations when the component changes rarely. The manual strategy, enabled via Query::without_change_detection(), removes automatic invalidation, potentially improving frame‑time but risking stale results if developers forget to refresh the query.
This trade‑off becomes critical in systems with many entities and complex queries, where the cost of recomputation can outweigh the risk of stale data. The current documentation notes that the decision between these approaches is unresolved, and the best choice may vary by use case.
Which approach balances correctness and performance for a given workload?
What measurable criteria (e.g., entity count, query complexity, update frequency) should guide that choice?
How can developers systematically detect and mitigate stale cache scenarios when using manual refresh?
Answer: Use automatic invalidation as the default for Bevy ECS queries that involve frequent component writes or cheap recomputation. Switch to manual refresh via Query::without_change_detection() only when profiling shows that a query's recomputation cost dominates the frame budget and component updates are rare enough that an explicit refresh can amortize that cost.
Query::without_change_detection() applies to the entire query, not a single component, so it can unintentionally disable change detection for other tracked components.When component changes are frequent or queries are cheap, the overhead of change-detection bookkeeping is small compared with the risk of acting on stale data, making automatic invalidation the safer choice. When changes are infrequent and query recomputation is expensive, disabling automatic invalidation and refreshing explicitly can save noticeable frame time.
| Criterion | Favors automatic invalidation | Favors manual refresh |
|---|---|---|
| Tracked component writes per frame | Frequent or close to the frame rate | Rare compared with the frame rate |
| Query recomputation cost | Cheap relative to frame budget | Expensive and profiled as a bottleneck |
| Entity count and archetype fragmentation | Small or simple queries | Large complex queries only after profiling |
| Stale-data tolerance | Any stale frame is a bug | A bounded stale window is acceptable, such as diagnostics or analytics |
Query::without_change_detection() and insert an explicit refresh such as refresh_if_changed after a write batch or at a fixed simulation step; verify the method exists in your Bevy release.Before adopting manual refresh for a specific workload, confirm the tracked-component write rate per query per frame and the cost of one recomputation as a fraction of the frame budget. If writes are frequent or recomputation is cheap, manual refresh is not recommended.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 29 May 2025, 09:54 UTC
In Bevy, QueryState caches archetypes that satisfy a query. Adding or removing a component moves an entity to a new archetype, triggering cache updates. However, mutating a component's value via Mut leaves the archetype unchanged, so the cache remains valid.
Changed and Added filters do not rebuild the query cache. During iteration, they compare each component's change tick against the system's last-run tick. The cost scales with matched entities, not cache invalidation.
Ref for read-only access to avoid marking components as changedChanged filters for systems that genuinely need value change reactionsThe performance lever is reducing unnecessary tick comparisons and structural changes, not manual cache refresh.