Storages

Specs contains a bunch of different storages, all built and optimized for different use cases. But let's see some basics first.

Storage basics

What you specify in a component impl-block is an UnprotectedStorage. Each UnprotectedStorage exposes an unsafe getter which does not perform any checks whether the requested index for the component is valid (the id of an entity is the index of its component). To allow checking them and speeding up iteration, we have something called hierarchical bitsets, provided by hibitset.

Note: In case you don't know anything about bitsets, you can safely skip the following section about it. Just keep in mind that we have some mask which tracks for which entities a component exists.

How does it speed up the iteration? A hierarchical bitset is essentially a multi-layer bitset, where each upper layer "summarizes" multiple bits of the underlying layers. That means as soon as one of the underlying bits is 1, the upper one also becomes 1, so that we can skip a whole range of indices if an upper bit is 0 in that section. In case it's 1, we go down by one layer and perform the same steps again (it currently has 4 layers).

Storage overview

Here a list of the storages with a short description and a link to the corresponding heading.

Storage TypeDescriptionOptimized for
BTreeStorageWorks with a BTreeMapno particular case
DenseVecStorageUses a redirection tablefairly often used components
HashMapStorageUses a HashMaprare components
NullStorageCan flag entitiesdoesn't depend on rarity
VecStorageUses a sparse Vec, empty slots are uninitializedcommonly used components
DefaultVecStorageUses a sparse Vec, empty slots contain Defaultcommonly used components

Slices

Certain storages provide access to component slices:

Storage TypeSlice typeDensityIndices
DenseVecStorage&[T]DenseArbitrary
VecStorage&[MaybeUninit<T>]SparseEntity id()
DefaultVecStorage&[T]SparseEntity id()

This is intended as an advanced technique. Component slices provide maximally efficient reads and writes, but they are incompatible with many of the usual abstractions which makes them more difficult to use.

BTreeStorage

It works using a BTreeMap and it's meant to be the default storage in case you're not sure which one to pick, because it fits all scenarios fairly well.

DenseVecStorage

This storage uses two Vecs, one containing the actual data and the other one which provides a mapping from the entity id to the index for the data vec (it's a redirection table). This is useful when your component is bigger than a usize because it consumes less RAM.

DenseVecStorage<T> provides as_slice() and as_mut_slice() accessors which return &[T]. The indices in this slice do not correspond to entity IDs, nor do they correspond to indices in any other storage, nor do they correspond to indices in this storage at a different point in time.

HashMapStorage

This should be used for components which are associated with very few entities, because it provides a lower insertion cost and is packed together more tightly. You should not use it for frequently used components, because the hashing cost would definitely be noticeable.

NullStorage

As already described in the overview, the NullStorage does itself only contain a user-defined ZST (=Zero Sized Type; a struct with no data in it, like struct Synced;). Because it's wrapped in a so-called MaskedStorage, insertions and deletions modify the mask, so it can be used for flagging entities (like in this example for marking an entity as Synced, which could be used to only synchronize some of the entities over the network).

VecStorage

This one has only one vector (as opposed to the DenseVecStorage). It just leaves uninitialized gaps where we don't have any component. Therefore it would be a waste of memory to use this storage for rare components, but it's best suited for commonly used components (like transform values).

VecStorage<T> provides as_slice() and as_mut_slice() accessors which return &[MaybeUninit<T>]. Consult the Storage::mask() to determine which indices are populated. Slice indices cannot be converted to Entity values because they lack a generation counter, but they do correspond to Entity::id()s, so indices can be used to collate between multiple VecStorages.

DefaultVecStorage

This storage works exactly like VecStorage, but instead of leaving gaps uninitialized, it fills them with the component's default value. This requires the component to impl Default, and it results in more memory writes than VecStorage.

DefaultVecStorage provides as_slice() and as_mut_slice() accessors which return &[T]. Storage::mask() can be used to determine which indices are in active use, but all indices are fully initialized, so the mask() is not necessary for safety. DefaultVecStorage indices all correspond with each other, with VecStorage indices, and with Entity::id()s.