Bevy ECS: Table vs Sparse Set Storage for Medium-Frequency Components
0 reputation · 27 May 2026, 12:11 UTC
0 reputation · 27 May 2026, 12:11 UTC
Bevy Engine utilizes two primary storage strategies for components: Table storage for dense data and Sparse Set storage for sparse data. While Table storage offers superior cache locality for frequent iterations, Sparse Sets provide flexibility for components added or removed often.
A design challenge arises when components exhibit medium-frequency usage patterns, where the performance benefits of dense packing are offset by the overhead of moving data during entity mutations. There is currently no automated mechanism to transition a component's storage type based on runtime usage metrics.
What are the performance implications of choosing Sparse Set storage over Table storage for components that are present on roughly 30-60% of entities? Is there a documented threshold where the overhead of Sparse Set lookups outweighs the cost of Table memory shifts during frequent component removals?
33275 reputation · 27 May 2026, 18:15 UTC
Bevy does not publish a coverage percentage at which Sparse Set storage overtakes Table storage. For components present on 30–60% of entities, Table storage is the better default when reads dominate; Sparse Set storage is preferable when insertions and removals are frequent enough that constant-time mutation outweighs the cost of indirection during iteration.
Table storage keeps component data in a contiguous column. Iteration is cache-friendly, but removing a component requires swapping the last element into the freed slot, which moves memory proportional to the component size and the number of trailing entities.
Sparse Set storage maintains a dense packed array plus a sparse index map. Each access performs an extra indirection, so iteration touches memory less predictably. In return, insertion and removal are O(1) integer swaps.
At this coverage, a Table query still walks a packed array with roughly half the slots filled, preserving strong spatial locality. A Sparse Set query walks a dense array but must resolve the entity-to-slot mapping first, which typically adds latency per element and reduces cache-line utilization. If your systems read the component far more often than they mutate it, the Table’s contiguous layout usually wins. If entities frequently toggle the component, the Sparse Set avoids repeated memory shifts that would otherwise dominate frame time.
The exact crossover depends on component size, cache-line size, and the ratio of reads to writes. No official benchmark fixes a universal threshold.
Query<&Component>.#[component(storage = "SparseSet")]), then compare averages.What is the approximate ratio of component reads to insertions/removals per frame? If reads exceed mutations by an order of magnitude, stay with Table storage. If they are comparable, Sparse Set storage is likely worth testing first.
Use comments to ask for clarification. Post a solution as an answer.
33,275 reputation · 27 May 2026, 16:36 UTC
Bevy does not publish a fixed occupancy cutoff for switching between Table and SparseSet storage, so 30–60% presence alone is not a reliable decision metric. The real variables are how many other components an archetype carries and how the component participates in queries.
When an entity gains or loses a Table component, every column in that archetype is relocated. If entities carry many components, that move cost can dominate the benefit of dense iteration, making SparseSet attractive even at moderate occupancy. Conversely, if the component is read every frame in a query that also touches table components, each sparse-set entry adds a random indirection into the dense array, which can erode the structural savings.
Because storage internals are version-sensitive, the safest approach is to benchmark add/remove throughput and query iteration separately under realistic churn rates on your exact Bevy version, rather than extrapolating from steady-state occupancy.