Sub Allocators

A sub allocator will always be aligned to DefaultAlignment, this is done so we don't have to keep a matrix of allocation size and alignment when re-using a block of memory. If an allocation has some custom alginment, it will use the default page allocator, not the sub-allocator.

Allocating memory

The sub allocator needs to know the block size and the free list to release blocks back into. Start implementing the allocate function by checking if there are any free blocks available. If there are no free blocks: we need to reserve one page of memory, initialize as many blocks as it will hold, and add each block to the free list. Basically, if the free list is empty, we populate it before the next step.

void* SubAllocate(u32 requestedBytes, u32 blockSize, Allocation** freeList, const char* location, Allocator* allocator) {
    bool grabNewPage = *freeList == 0;
    if (*freeList == 0) {
        // Find and reserve 1 free page
        const u32 page = FindRange(allocator, 1, allocator->scanBit);
        SetRange(allocator, page, 1);

        // Zero out the pages memory
        u8* mem = (u8*)allocator + allocator->pageSize * page;
        Set(mem, 0, allocator->pageSize, __LOCATION__);

        // Figure out how many blocks fit into this page
        const u32 numBlocks = allocator->pageSize / blockSize;

        // For each block in this page, initialize it's header
        for (u32 i = 0; i < numBlocks; ++i) {
            Allocation* alloc = (Allocation*)mem;
            mem += blockSize;

            // Initialize the allocation header
            alloc->prev = 0;
            alloc->size = 0;
            alloc->next = *freeList;
            alloc->alignment = 0;
            alloc->location = location;

            if (*freeList != 0) {
                (*freeList)->prev = alloc;
            }
            *freeList = alloc;
        }
    }

Now that there is guaranteed to be a free block, we pop a free block off the free list and add it to the allocators active list. Finally, we finish the function by returning the memory. This would be a good spot to clear out the memory if you want your allocator to behave like that.

    // At this point we know the free list has some number of blocks in it. 
    // Save a reference to the current header & advance the free list
    // Advance the free list, we're going to be using this one.
    Allocation* block = *freeList;
    if ((*freeList)->next != 0) {
        (*freeList)->next->prev = 0;
    }
    *freeList = (*freeList)->next;
    block->prev = 0;
    block->size = requestedBytes;
    block->location = location;
    block->alignment = 0;

    // Track the sub allocator
    block->next = allocator->active;
    if (allocator->active != 0) {
        allocator->active->prev = block;
    }
    allocator->active = block;

    if (allocator->allocateCallback != 0) {
        u64 firstPage = (u64)(((u8*)block - (u8*)allocator) / allocator->pageSize);
        allocator->allocateCallback(allocator, block, requestedBytes, blockSize, firstPage, grabNewPage? 1 : 0);
    }

    // Memory always follows the header
    return (u8*)block + sizeof(Allocation);
}

Releasing memory

To release memory form the sub-allocator, first find the allocation header. It is stored immediateley before the memory being freed. Check the size of the allcoation, we are dealing with a double free if the sie is 0. If we are dealing with a double free, return early, otherwise set the size of the header to 0 which will signal that this allocation has already been released.

void SubRelease(void* memory, u32 blockSize, Allocation** freeList, const char* location, Allocator* allocator) {
    // Find the allocation header and mark it as free. Early out on double free to avoid breaking.
    Allocation* header = (Allocation*)((u8*)memory - sizeof(Allocation));
    if (header->size == 0) {
        return;
    }
    u32 oldSize = header->size; // Hold on to for callback
    header->size = 0;

Next, we need to do some housekeeping. Remove the allocation from the active list of the allocator, and add it to the provided free list.

    // Now remove from the active list.
    if (header == allocator->active) { // Removing head
        if (allocator->active->next != 0) {
            allocator->active->next->prev = 0;
        }
        allocator->active = allocator->active->next;
    }
    else { // Removing link
        if (header->next != 0) {
            header->next->prev = header->prev;
        }
        if (header->prev != 0) {
            header->prev->next = header->next;
        }
    }

    // Add memory back into the free list
    if (*freeList != 0) {
        (*freeList)->prev = header;
    }
    header->next = *freeList;
    header->prev = 0;
    *freeList = header;

Next, we need to check every block in the page. If all of the blocks are free, we release the page. If any of the blocks are in use, the page will remain used. This next bit of code loops trough all the blocks for the current page, and if any of them have a size greater than 0, the page will not be released.

    // Find the first allocation inside the page
    u64 startPage = (u64)((u8*)header - (u8*)allocator) / allocator->pageSize;
    u8* mem =(u8*)allocator + startPage * allocator->pageSize;

    // Each sub allocator page contains multiple blocks. check if all of the blocks 
    // belonging to a single page are free, if they are, release the page.
    bool releasePage = true;
    
    const u32 numAllocationsPerPage = allocator->pageSize / blockSize;
    for (u32 i = 0; i < numAllocationsPerPage; ++i) {
        Allocation* alloc = (Allocation*)mem;
        if (alloc->size > 0) {
            releasePage = false;
            break;
        }
        mem += blockSize;
    }

Finally, we finish the function by releasing the page if appropraite. Before a page can be free'd, we need to remove all of it's blocks from the allocators free list.

    // If appropriate, release entire page
    if (releasePage) {
        // Remove from free list
        mem = (u8*)allocator + startPage * allocator->pageSize;
        for (u32 i = 0; i < numAllocationsPerPage; ++i) {
            Allocation* iter = (Allocation*)mem;
            mem += blockSize;

            if (*freeList == iter) { // Removing head, advance list
                *freeList = (*freeList)->next;
                if ((*freeList) != 0) {
                    (*freeList)->prev = 0;
                }
            }
            else { // Unlink not head
                if (iter->next != 0) {
                    iter->next->prev = iter->prev;
                }
                if (iter->prev != 0) {
                    iter->prev->next = iter->next;
                }
            }
            iter->prev = 0;
            iter->next = 0;
        }

        // Clear the tracking bits
        ClearRange(allocator, startPage, 1);
    }

    if (allocator->releaseCallback != 0) {
        allocator->releaseCallback(allocator, header, oldSize, blockSize, startPage, releasePage ? 1 : 0);
    }
}

Calling the sub-allocator

Now that the sub-allocator can allocate and release memory, let's actually call it. Note that only allocations with the default alignment take advantage of the sub-allocator. In the Allocate function, right after finding the final allocationSize, if the allocation size is small enough, forward the cal to the sub allocator.

if (alignment == 0) {
    if (allocationSize <= 64) {
        return SubAllocate(64, &allocator->free_64, location, allocator);
    }
    else if (allocationSize <= 128) {
        return SubAllocate(128, &allocator->free_128, location, allocator);
    }
    else if (allocationSize <= 256) {
        return SubAllocate(256, &allocator->free_256, location, allocator);
    }
    else if (allocationSize <= 512) {
        return SubAllocate(512, &allocator->free_512, location, allocator);
    }
    else if (allocationSize <= 1024) {
        return SubAllocate(1024, &allocator->free_1024, location, allocator);
    }
    else if (allocationSize <= 2048) {
        return SubAllocate(2048, &allocator->free_2048, location, allocator);
    }
}

Releasing the memory is very similar, in the Free function, right after finding the final paddedAllocationSize, call the sub allocator release function if the allocation size is small enough.

if (alignment == 0) {
    if (paddedAllocationSize <= 64) {
        SubRelease(memory, 64, &allocator->free_64, location, allocator);
        return;
    }
    else if (paddedAllocationSize <= 128) {
        SubRelease(memory, 128, &allocator->free_128, location, allocator);
        return;
    }
    else if (paddedAllocationSize <= 256) {
        SubRelease(memory, 256, &allocator->free_256, location, allocator);
        return;
    }
    else if (paddedAllocationSize <= 512) {
        SubRelease(memory, 512, &allocator->free_512, location, allocator);
        return;
    }
    else if (paddedAllocationSize <= 1024) {
        SubRelease(memory, 1024, &allocator->free_1024, location, allocator);
        return;
    }
    else if (paddedAllocationSize <= 2048) {
        SubRelease(memory, 2048, &allocator->free_2048, location, allocator);
        return;
    }
}

Super Allocators

Depenging on your allocation needs, a super allocator might also make sense. Like a sub-allocator, a super allocator also cuts up pages into smaller fixed size allocations. Unlike the sub-allocator, a super allocator can span multiple pages. This helps with occupancy, consider allocating a 2048 bytes, there would be (4096 - 64 - 2048) = 1984 bytes lost. But if the super allocator used two pages, there would be (4096 * 2 - 64 - 2048) = 6080 bytes left, which is enough for 2 more allocations. This way, the super allocator manages to use only two pages for 3 blocks.

When the memory is being released, finding the starting page can be a bit tricky with super allocators. The convention i'd recommend is to align the page index to the number of pages being reserved for each allocation. So, if in the above example we reserve two pages for a three block allocation, the index of the first page would always be a multiple of 2 (or however many pages are being used). I've never had the need to implement a super allocator for any project.