- Introduction
- Key Features
- Cost
- Using it from C
- Using it from C++
- Growing a block at the front
- API
- Configuration
- What this library does not do
- Supported platforms
- Setting up
- Building and testing
- License
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 );
}- 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
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.
#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().
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 backstatic tiny::Pool<1024> pool;
Node *n = pool.create<Node>(); // allocate + construct
pool.destroy(n); // destruct + freeBuild 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 whytiny_mm_new.cppcompiles to nothing unless the macro is defined. For Arduino, useTINY_MM_CLASS_ALLOCATORinstead. -
Do not lean on
newreturningNULL. With-fno-exceptionsthat 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-newfixes; clang additionally foldsp == NULLto false, and both are free to skip the allocation of anewexpression 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_allocis thrown instead, and thestd::nothrowoverloads 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.
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.
| 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 |
| 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.
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(), notmm_free_size(). intsized. A pool cannot exceedINT_MAXbytes, which no target this library is aimed at will notice.
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 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
- Download source from https://github.com/lexus2k/tiny_mm
- Build the library
- cd tiny_mm && make
- Link library to your project
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.
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.
Copyright (c) 2018-2026 Alexey Dynda
See LICENSE for the full text.