#[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 or NonLinear, 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 many PoolAllocators 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 PoolAllocators 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>

source

pub fn new(region: MemoryAlloc) -> Arc<Self>

Creates a new PoolAllocator for the given region.

Panics
source

pub fn block_size(&self) -> DeviceSize

Size of a block. Can be bigger than BLOCK_SIZE due to alignment requirements.

source

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().

source

pub fn free_count(&self) -> usize

Number of free blocks.

Trait Implementations§

source§

impl<const BLOCK_SIZE: DeviceSize> Debug for PoolAllocator<BLOCK_SIZE>

source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
source§

impl<const BLOCK_SIZE: DeviceSize> DeviceOwned for PoolAllocator<BLOCK_SIZE>

source§

fn device(&self) -> &Arc<Device>

Returns the device that owns Self.

Auto Trait Implementations§

§

impl<const BLOCK_SIZE: u64> !RefUnwindSafe for PoolAllocator<BLOCK_SIZE>

§

impl<const BLOCK_SIZE: u64> Send for PoolAllocator<BLOCK_SIZE>

§

impl<const BLOCK_SIZE: u64> Sync for PoolAllocator<BLOCK_SIZE>

§

impl<const BLOCK_SIZE: u64> Unpin for PoolAllocator<BLOCK_SIZE>

§

impl<const BLOCK_SIZE: u64> !UnwindSafe for PoolAllocator<BLOCK_SIZE>

Blanket Implementations§

source§

impl<T> Any for Twhere T: 'static + ?Sized,

source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
source§

impl<T> Borrow<T> for Twhere T: ?Sized,

const: unstable · source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
source§

impl<T> BorrowMut<T> for Twhere T: ?Sized,

const: unstable · source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
source§

impl<T> From<T> for T

const: unstable · source§

fn from(t: T) -> T

Returns the argument unchanged.

source§

impl<T, U> Into<U> for Twhere U: From<T>,

const: unstable · source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

source§

impl<T, U> TryFrom<U> for Twhere U: Into<T>,

§

type Error = Infallible

The type returned in the event of a conversion error.
const: unstable · source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
source§

impl<T, U> TryInto<U> for Twhere U: TryFrom<T>,

§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
const: unstable · source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.