Allocators

The Pt::Allocator interface can be used to optimize or customize allocation strategies. Two allocators are provided, which can be approached by the Allocator interface, a pool based allocator and a page based allocator. A pool based allocator is beneficial in all cases where many small objects of small sizes are created. This is for example used to optimize memory usage during serialization. The page based allocator simply places data consecutively in memory and frees the whole block when its no longer in use. This is useful in situations where chunks of memory or objects are created and destroyed at the same time.

The Allocator Interface

Allocators allow a program to use different methods of allocating and deallocating raw memory. The default implementation will simply use new and delete. Custom allocators are implemented by overriding the two methods allocate() and deallocate() of the Pt::Allocator base class. The following example tracks the amount of allocated memory:

class CheckedAllocator : public Pt::Allocator
{
public:
CheckedAllocator()
: _allocated(0)
{}
virtual void* allocate(std::size_t size)
{
void* p = Pt::Allocator::allocate(size);
_allocated += size;
return p;
}
virtual void deallocate(void* p, std::size_t size)
{
_allocated -= size;
}
std:size_t allocated() const
{ return _allocated; }
private:
std::size_t _allocated;
};

This interface differs from std::allocator used for STL containers, because it allows to allocate memory of different sizes through the same interface. The std::allocator is meant to allocate and also construct objects of the same size. It is however possible, to implement a std::allocator using the raw memory allocators described here.

Pool Allocation

The PoolAllocator uses pools to allocate memory. Each pool consists of blocks of equally sized records, which can be used for allocations up to the size of a record. The record sizes increase from pool to pool. When memory is allocated, a record is used from the pool, which handles the requested size. When memory is deallocated, the record is returned to the corresponding pool. This method of allocation is effective, because larger blocks of memory are allocated and then reused in the form of many smaller records. An advantage of this kind of allocator, compared to free list based allocators, is that it is able to release completely unused blocks.

// Contruct with max. record size, alignment and block size
Pt::PoolAllocator allocator(32, 8, 4096);
// will use a record from the pools
void* p1 = allocator.allocate( sizeof(float) );
// too large, will use operator new
void* p2 = allocator.allocate( 64 );

When a PoolAllocator is constructed, the maximum size for records has to be specified. The reason for this is that this type of allocator is ineffective for large allocations. Therefore, memory which is larger than this limit will be allocated using the new operator, instead of a record from a memory pool. Optionally, the alignment and the maximum block size can be set. The record sizes of the pools will be multiples of the alignment. So if the alignment is 8, the first pool will have records of size 8, the second pool records of size 16 and so forth, until the maximum size is reached. The maximum block size controls the number of records per block. A new block of records is added, when a pool is depleted and has to be extended to allow more allocations.

If memory of uniform sizes has to be allocated, a MemoryPool can be used directly, rather than indirectly as part of the PoolAllocator. This can be faster, because the PoolAllocator has to look up the pool for the requested size of memory each time it allocates and deallocates. To construct a MemoryPool, the size of the records, i.e. the size of memory it can allocate, has to be specified.

Pt::MemoryPool pool( sizeof(float), 4096 );
void* p = pool.allocate();
float* f = new (p) float(3.1415);
pool.deallocate(f);

Optionally, the maximum size of the blocks in the pool can be controlled. In the example shown above, the pool can only allocate memory of the size required for a float. Each time the pool itself requires more memory, it will allocate a new block of 4096 bytes.

Page Allocation

The PageAllocator is useful, when chunks of memory have to be allocated, that can be released simultaneously. This allows the PageAllocator to allocate memory consecutively on pages and simply release all pages together at the end. Therefore, deallocate() will not do anything, but memory will only eventually be released, when clear() is called or the PageAllocator is destructed. The next example illustrates this:

void useAllocator(Pt::Allocator& a)
{
for(std::size_t n = 1; n < 16; ++n)
{
void* p = a.allocate(n);
...
// won't do anything if it's a PoolAllocator
a.deallocate(p, n);
}
}
useAllocator(allocator);
// release all allocated memory
allocator.clear();