Skip to content

Repository files navigation

Tiny Memory Manager

build License: MIT

Introduction

Tiny memory manager allocates and frees blocks inside a buffer you provide. It is meant for microcontrollers, where malloc() is either missing, or present but unwelcome: an unbounded heap that grows towards the stack is hard to reason about, and a fragmented one fails at 3am rather than at compile time.

A pool is a plain array. Its size is the worst case memory use of everything allocated from it, and you can see that number in your source code.

#include "tiny_mm.h"

uint8_t pool[512];

int main()
{
    mm_init( pool, sizeof(pool) );
    void *p = mm_alloc( pool, 20 );
    mm_free( pool, p );
}

Key Features

  • Simple API and easy to use
  • Fixed pool: no system heap, no malloc, no surprises about peak memory
  • As many independent pools as you like, each with its own lifetime
  • Blocks can be resized at the end (mm_resize) or at the front (mm_prepend / mm_resize_head), the latter without copying the payload
  • Can back C++ new / delete, either for a single class or program wide, so real C++ is usable on an AVR without a heap
  • Free blocks are coalesced on both sides, so a free/alloc cycle is not a one way trip into fragmentation
  • mm_check() validates the pool, and the test suite compares every live block against a shadow copy after every single operation
  • Small: 5 bytes of overhead per allocation on AVR, and about 350 bytes of flash for init/alloc/free

Cost

Per allocation, on top of the bytes you asked for:

Target MM_ALIGNMENT MM_BLOCK_OVERHEAD
AVR (16-bit pointers) 1 5 bytes
32-bit MCU (ARM, Xtensa, ...) 8 16 bytes
64-bit host 8 24 bytes

The same overhead applies to each hole between allocations, and requests are rounded up to MM_ALIGNMENT. MM_BLOCK_OVERHEAD is a compile time constant, so a pool can be sized exactly:

/* room for 10 nodes and nothing more */
static uint8_t pool[ MM_POOL_SIZE(10, sizeof(node_t)) ];

Code size, measured on an atmega328p with -Os -ffunction-sections -Wl,--gc-sections, over an empty program:

Functions used Flash
mm_init + mm_alloc + mm_free ~344 bytes
plus mm_resize and mm_prepend ~1.3 KB

Anything you do not call is dropped by the linker, so mm_check(), mm_walk() and the resize operations cost nothing unless you use them.

Using it from C

#include "tiny_mm.h"

static uint8_t pool[512];

if ( mm_init(pool, sizeof(pool)) != 0 )
{
    /* the buffer is too small to hold even an empty pool */
}

void *p = mm_alloc(pool, 20);
if ( p == NULL )
{
    /* the pool is full: this is the only failure mode, and it is local */
}

p = mm_resize(pool, p, 40);   /* grows at the end, may relocate and copy */
mm_free(pool, p);

mm_free_size(pool);           /* bytes free in total */
mm_largest_free_size(pool);   /* bytes free in one piece, which is what fits */

mm_free(pool, NULL) is a no-op, like free().

Using it from C++

One class at a time

TINY_MM_CLASS_ALLOCATOR gives a single class its own new and delete. The rest of the program, including the Arduino core, keeps using the system heap, so there is nothing to clash with:

#include "tiny_mm.hpp"

static uint8_t node_pool[512];

class Node
{
public:
    TINY_MM_CLASS_ALLOCATOR(node_pool)

    Node(int v) : value(v), next(NULL) {}
    int value;
    Node *next;
};

mm_init(node_pool, sizeof(node_pool));

Node *n = new Node(42);   // comes out of node_pool
delete n;                 // destructor runs, memory goes back

A pool that owns its storage

static tiny::Pool<1024> pool;

Node *n = pool.create<Node>();   // allocate + construct
pool.destroy(n);                 // destruct + free

Program wide new and delete

Build src/tiny_mm_new.cpp with -DTINY_MM_OVERRIDE_GLOBAL_NEW and every plain new in the program allocates from the pool you install:

static uint8_t pool[2048];

void setup()
{
    mm_init(pool, sizeof(pool));
    mm_set_default_pool(pool);
}

Two things to know before you switch this on:

  • It does not work as a drop-in Arduino library. The Arduino core already defines the global operators in its own new.cpp, and two definitions are a duplicate symbol. That is why tiny_mm_new.cpp compiles to nothing unless the macro is defined. For Arduino, use TINY_MM_CLASS_ALLOCATOR instead.

  • Do not lean on new returning NULL. With -fno-exceptions that is what tiny_mm does, but the language lets a compiler assume otherwise. By default gcc runs the constructor on the null pointer anyway, which -fcheck-new fixes; clang additionally folds p == NULL to false, and both are free to skip the allocation of a new expression whose object never escapes. So build with -fcheck-new, and where a failed allocation is a case you actually handle, allocate explicitly instead:

    Node *n = pool.create<Node>();   // checks for you, then placement-new
    if ( n == NULL ) { /* really out of memory */ }

    With exceptions enabled std::bad_alloc is thrown instead, and the std::nothrow overloads are provided.

Types with extended alignment (alignas(32) and friends) still go through the platform's aligned operator new; tiny_mm does not intercept those.

Growing a block at the front

This is the operation the library has that realloc() does not. When a protocol stack has a payload and needs to put a header in front of it, the usual answer is to allocate a bigger buffer and copy. mm_prepend() instead walks backwards into the free block in front and moves the block header, so the payload never moves:

uint8_t *payload = mm_alloc(pool, 100);
/* ... fill in the payload ... */

uint8_t *frame = mm_prepend(pool, payload, 4);   /* 4 bytes of room in front */
if ( frame != NULL )
{
    memcpy(frame, header, 4);   /* the payload is at frame + 4, untouched */
}

If there is no free space in front, the call falls back to allocate-and-copy, so it always succeeds when the pool has room, it just is not always free.

mm_resize_head() is the lower level form, and it counts in absolute sizes rather than in added bytes. Because mm_alloc() may round a request up, use mm_block_size() to learn what a block really holds before computing offsets against it, or just use mm_prepend(), which states the offset directly.

API

Function Purpose
mm_init prepare a buffer for use as a pool
mm_alloc allocate a block
mm_free release a block, coalescing with its free neighbours
mm_resize resize, adding or removing bytes at the end
mm_prepend make room for N bytes in front of an existing block
mm_resize_head resize, adding or removing bytes at the front
mm_block_size usable size of a block, which may exceed the request
mm_free_size total free bytes
mm_largest_free_size biggest block that can still be allocated
mm_walk iterate over every block, with no dependencies
mm_check validate the pool, for tests and for hunting corruption
mm_print dump the pool to stderr (hosted builds only)
mm_set_default_pool choose the pool behind the global C++ operators

Configuration

Macro Default Meaning
MM_ALIGNMENT 1 on AVR, 8 elsewhere alignment of every returned pointer
MM_ENABLE_PRINT 0 on MCUs, 1 on hosts compile mm_print() and pull in stdio
TINY_MM_OVERRIDE_GLOBAL_NEW off replace the global C++ new/delete

MM_ALIGNMENT may not be lowered below the natural alignment of a pointer on your target, because the block headers themselves sit on those boundaries; the library refuses to compile if you try.

What this library does not do

Being honest about the edges is cheaper than a bug report:

  • Not thread safe and not interrupt safe. There is no locking anywhere. If two contexts touch the same pool, serialize them yourself. Separate pools in separate contexts are fine, since no state is shared between pools beyond the default-pool pointer.
  • Allocation is O(n) in the number of blocks. It is a first fit walk over the block list, with no free list and no size classes. That is the right trade for a pool with tens of blocks and the wrong one for thousands.
  • No protection against your own bugs. Block headers live inline between the blocks, so writing past the end of an allocation corrupts the pool. mm_check() will tell you afterwards; it cannot prevent it. Run your tests under AddressSanitizer.
  • No defragmentation. Blocks do not move behind your back, which is what makes raw pointers safe, and also what means a fragmented pool stays fragmented. Watch mm_largest_free_size(), not mm_free_size().
  • int sized. A pool cannot exceed INT_MAX bytes, which no target this library is aimed at will notice.

Supported platforms

Any platform with a C99 compiler. Built and tested in CI on gcc and clang (64-bit and 32-bit), and cross compiled for atmega328p with avr-gcc. The C++ layer is tested against C++11 through C++20, with and without exceptions.

Setting up

Setting up for Arduino

  • Download source from https://github.com/lexus2k/tiny_mm
  • Put the sources to Arduino/libraries/ folder
  • See examples/ for a sketch using a pool, and one putting C++ objects in it

Setting up for ESP32 IDF

  • Put the sources to the components/ folder of your project

Compiling with gcc

Building and testing

make                # build libtiny_mm.a
make check          # functional tests of the C API
make check-stress   # 200k randomized operations against a shadow copy
make check-cpp      # tiny::Pool and TINY_MM_CLASS_ALLOCATOR
make check-new      # global new/delete replacement
make check-all      # all of the above
make check-asan     # all of the above under ASan and UBSan

The stress test is the one that matters. It drives the pool through a long random sequence of alloc/free/resize/prepend and, after every operation, checks both that mm_check() still passes and that every live block still contains exactly the bytes that were written into it. Structural checks alone will happily accept an allocator that hands the same bytes to two owners.

License

The library is free and MIT licensed, so it can go into a commercial product without conditions. If this project helps you, you can give me a cup of coffee. Donate via Paypal

Copyright (c) 2018-2026 Alexey Dynda

See LICENSE for the full text.

About

Tiny Memory Manager

Topics

Resources

Code of conduct

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages