6
\$\begingroup\$

My plan is to allocate a fixed amount of static memory at initialization (a few MBs), and use a stack allocator for quick temporary buffers. The idea is to avoid calls to malloc() for these kinds of temporary buffers (should be safer this way), but still being able to return it from a function (and then freed) if needed, which I otherwise wouldn't be able to with local arrays.

An allocator based on a stack sounded great for this use case, and should also be quite fast since we're only incrementing a stack pointer and calling memset() to zero out the space.

The data I'll be storing on the stack will always be of type uint8_t, so the allocator assumes that's what we want and returns a uint8_t* instead of a void* like malloc() does.

#include <stdint.h>
#include <string.h>

// Convert megabytes to bytes.
#define MB(x) (x * 1024 * 1024)

// Default size: 2 MB.
static uint8_t Stack[MB(2)];

// Points to the next available memory address.
static uint8_t *StackPointer = Stack;

int32_t GetStackUsage()
{
    // Return the amount of memory used.
    return StackPointer - Stack;
}

int32_t GetStackFree()
{
    // Return the amount of available memory.
    return sizeof(Stack) - GetStackUsage();
}

uint8_t *Alloc(int32_t length)
{
    // Return a null pointer, if memory requested is less than 1 byte.
    if (length < 1) return NULL;

    // Return a null pointer, if there is not enough memory left on the stack.
    if (StackPointer + length > Stack + sizeof(Stack)) return NULL;

    // Get a pointer to the next available address.
    uint8_t *memory = StackPointer;

    // Zero out the memory.
    memset(memory, 0, length);
    
    // Increment stack pointer.
    StackPointer += length;

    return memory;
}

void Dealloc(uint8_t *memory)
{
    // Double free is not a good idea.
    if (memory == NULL) return;

    // Reset stack pointer.
    StackPointer = memory;
}
\$\endgroup\$

1 Answer 1

9
\$\begingroup\$

Bug: Invalid address computation

StackPointer + length > Stack + sizeof(Stack) for this to be true, StackPointer + length points past the end of Stack + sizeof(Stack). Yet that calculation is undefined behavior (UB) as address calculations are only allowed within the object (or 1 past).

length > sizeof(Stack) - (StackPointer - Stack) works better.

Why type int32_t?

Allocation sizes are idiomatically size_t in C. int32_t is unfounded.

I could see some rational for unsigned, uint_least32_t or even int, but not int32_t. size_t remains the best choice for sizing.

Consider 0 allocation

I would find such an allocator more useful where Alloc(0) did not return NULL. If needed, use if (length == 0) length++;. This way, a NULL return is unambiguously an out-of-memory failure.

Alignment

OP has "data I'll be storing on the stack will always be of type uint8_t", yet that is in the text and deserves to be in the code as a comment to indicate this restriction.

Consider instead, making useful for all types.

*alloc() and friends return a pointer suitable for all alignments. Research max_align_t to find the alignment needed.

Consider quantizing the length:

// Something like
if (length % alignof(max_align_t)) {
  length += alignof(max_align_t) - length % alignof(max_align_t);
}
// or 
length = (length + alignof(max_align_t) - 1) & (alignof(max_align_t) - 1);

When length is an unsigned type and since sizeof(max_align_t) is certainly a power of 2, length % sizeof(max_align_t) results in a fast and operation. So this adjustment is not a costly general purpose %.

Why return uint8_t *?

C allocators far more often return void *.

Speed

If "should also be quite fast", skip the memset(memory, 0, length); part and let the caller zero memory if desired.

Avoid precedence problems

Consider the effect of MB(2 + 3) --> (2 + 3 * 1024 * 1024)

Better as #define MB(x) ((x) * 1024 * 1024)

Avoid overflow

#define MB(x) (x * 1024 * 1024) is overflow with 16-bit int. Suggest:

#define MB(x) ((size_t)1024 * 1024 * (x))

Naming

Code has GetStackUsage, GetStackFree, Alloc, Dealloc which is scattered over that namespace and risks collisions.

Suggest AllocStack_Usage, AllocStack_Free, AllocStack, AllocStack_Free or some other uniform naming.

Header file

As this is a set of helper routines, form a AllocStack.h to show what is exposed to the world and a AllocStack.c to show the implementation.

Deallocating NULL

Code nicely handles Dealloc(NULL). This is like free(NULL) is OK.

Debugging idea

Dealloc() should expect that the pointer (after the null check) only grows the stack. If a Dealloc() occurs that shrank the stack, that is certainly an error and could be caught and flagged - maybe with an assert().

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.