Struct vulkano::memory::allocator::suballocator::PoolAllocator
source · #[repr(transparent)]pub struct PoolAllocator<const BLOCK_SIZE: DeviceSize> { /* private fields */ }
Expand description
A suballocator using a pool of fixed-size blocks as a free-list.
Since the size of the blocks is fixed, you can not create allocations bigger than that. You can create smaller ones, though, which leads to more and more internal fragmentation the smaller the allocations get. This is generally a good trade-off, as internal fragmentation is nowhere near as hard to deal with as external fragmentation.
See also the Suballocator
implementation.
Algorithm
The free-list contains indices of blocks in the region that are available, so allocation consists merely of popping an index from the free-list. The same goes for freeing, all that is required is to push the index of the block into the free-list. Note that this is only possible because the blocks have a fixed size. Due to this one fact, the free-list doesn’t need to be sorted or traversed. As long as there is a free block, it will do, no matter which block it is.
Since the PoolAllocator
doesn’t keep a list of suballocations that are currently in use,
resolving buffer-image granularity conflicts on a case-by-case basis is not possible.
Therefore, it is an all or nothing situation:
- you use the allocator for only one type of allocation,
Linear
orNonLinear
, or - you allow both but align the blocks to the granularity so that no conflics can happen.
The way this is done is that every suballocation inherits the allocation type of the region.
The latter is done by using a region whose allocation type is Unknown
. You are discouraged
from using this type if you can avoid it.
The block size can end up bigger than specified if the allocator is created with a region whose
allocation type is Unknown
. In that case all blocks are aligned to the buffer-image
granularity, which may or may not cause signifficant memory usage increase. Say for example
your driver reports a granularity of 4KiB. If you need a block size of 8KiB, you would waste no
memory. On the other hand, if you needed a block size of 6KiB, you would be wasting 25% of the
memory. In such a scenario you are highly encouraged to use a different allocation type.
The reverse is also true: with an allocation type other than Unknown
, not all memory within a
block may be usable depending on the requested suballocation. For instance, with a block size
of 1152B (9 * 128B) and a suballocation with alignment: 256
, a block at an odd index could
not utilize its first 128B, reducing its effective size to 1024B. This is usually only relevant
with small block sizes, as alignment requirements are usually rather small, but it completely
depends on the resource and driver.
In summary, the block size you choose has a signifficant impact on internal fragmentation due to the two reasons described above. You need to choose your block size carefully, especially if you require small allocations. Some rough guidelines:
- Always align your blocks to a sufficiently large power of 2. This does not mean your block size must be a power of two. For example with a block size of 3KiB, your blocks would be aligned to 1KiB.
- Prefer not using the allocation type
Unknown
. You can always create as manyPoolAllocator
s as you like for different allocation types and sizes, and they can all work within the same memory block. You should be safe from fragmentation if your blocks are aligned to 1KiB. - If you must use the allocation type
Unknown
, then you should be safe from fragmentation on pretty much any driver if your blocks are aligned to 64KiB. Keep in mind that this might change any time as new devices appear or new drivers come out. Always look at the properties of the devices you want to support before relying on any such data.
Efficiency
In theory, a pool allocator is the ideal one because it causes no external fragmentation, and both allocation and freeing is O(1). It also never needs to lock and hence also lends itself perfectly to concurrency. But of course, there is the trade-off that block sizes are not dynamic.
As you can imagine, the PoolAllocator
is the perfect fit if you know the sizes of the
allocations you will be making, and they are more or less in the same size class. But this
allocation algorithm really shines when combined with others, as most do. For one, nothing is
stopping you from having multiple PoolAllocator
s for many different size classes. You could
consider a pool of pools, by layering PoolAllocator
with itself, but this would have the
downside that the regions of the pools for all size classes would have to match. Usually this
is not desired. If you want pools for different size classes to all have about the same number
of blocks, or you even know that some size classes require more or less blocks (because of how
many resources you will be allocating for each), then you need an allocator that can allocate
regions of different sizes. You can use the FreeListAllocator
for this, if external
fragmentation is not an issue, otherwise you might consider using the BuddyAllocator
. On
the other hand, you might also want to consider having a PoolAllocator
at the top of a
hierarchy. Again, this allocator never needs to lock making it the perfect fit for a global
concurrent allocator, which hands out large regions which can then be suballocated locally on a
thread, by the BumpAllocator
for example.
Examples
Basic usage together with GenericMemoryAllocator
:
use std::sync::Arc;
use vulkano::memory::allocator::{
GenericMemoryAllocator, GenericMemoryAllocatorCreateInfo, PoolAllocator,
};
let memory_allocator = GenericMemoryAllocator::<Arc<PoolAllocator<{ 64 * 1024 }>>>::new(
device.clone(),
GenericMemoryAllocatorCreateInfo {
block_sizes: &[(0, 64 * 1024 * 1024)],
..Default::default()
},
)
.unwrap();
// Now you can use `memory_allocator` to allocate whatever it is you need.
Implementations§
source§impl<const BLOCK_SIZE: DeviceSize> PoolAllocator<BLOCK_SIZE>
impl<const BLOCK_SIZE: DeviceSize> PoolAllocator<BLOCK_SIZE>
sourcepub fn new(region: MemoryAlloc) -> Arc<Self>
pub fn new(region: MemoryAlloc) -> Arc<Self>
Creates a new PoolAllocator
for the given region.
Panics
- Panics if
region.size < BLOCK_SIZE
. - Panics if
region
is a dedicated allocation.
sourcepub fn block_size(&self) -> DeviceSize
pub fn block_size(&self) -> DeviceSize
Size of a block. Can be bigger than BLOCK_SIZE
due to alignment requirements.
sourcepub fn block_count(&self) -> usize
pub fn block_count(&self) -> usize
Total number of blocks available to the allocator. This is always equal to
self.region().size() / self.block_size()
.
sourcepub fn free_count(&self) -> usize
pub fn free_count(&self) -> usize
Number of free blocks.