Why malloc is doing more than I asked for — Senthilnathan
source: google images<br>techWhy malloc is doing more than I asked for<br>internal fragmentation in memory - explained<br>Jul 20, 2026<br>11 min read
void *p = malloc(13);
Almost all of us has seen this before when writing a safe c program. we know what this line does, atleast we know the purpose of it,<br>ask for 13 bytes, get 13 bytes<br>that’s what i thought when i started building this on a friday out of curiosity. 30 mins into it, without writing a single line of code… i knew that this almost never happens.<br>this simple line holds multiple interesting computations and allocations in itself under-the-hood.<br>for example, when I request 13 bytes.<br>actually reserved:<br>+--------+--------------+---------+-------------+<br>| Header | Back Pointer | Padding | User Memory |<br>+--------+--------------+---------+-------------+
the user memory is the only thing we are gonna use and the *p returns the start of that memory. all the others are just there… not for usage.. why is that??<br>i wondered the same, instead of going into theory.. let’s build a allocator and free.<br>the simplest possible allocator<br>the simplest possible allocator doesn’t even have a free(). it’s called a bump allocator - you give it a big chunk of memory upfront (an arena), and every allocation just… moves a pointer forward.<br>typedef struct {<br>uint8_t *cursor;<br>uint8_t *limit;<br>} Arena;
cursor is where the next allocation starts. limit is where the arena ends. that’s the whole design.<br>void *bump_alloc(Arena *a, size_t size) {<br>if (a->cursor + size > a->limit) return NULL; // out of memory<br>void *ptr = a->cursor;<br>a->cursor += size;<br>return ptr;
that’s it. you can only ever move forward. it’s the fastest possible allocator. but functionally, useless for anything long-running, because there’s no free(). you can only free the whole arena at once, never a single object inside it.<br>which raises the obvious question: if I can’t free one thing, how do I ever get those 13 bytes back?<br>turns out.. you can’t, not with this design. to get individual free() working, the allocator needs to remember something about every allocation it handed out. and that’s the moment metadata stops being optional.<br>why metadata<br>free(ptr) gets exactly one argument which is a pointer. if the allocator’s job is to reclaim that memory and make it reusable, it has to answer a question the caller never tells it:<br>how big was this allocation, and where does it actually begin?<br>the only way to answer that is to store the answer somewhere the allocator can find later, using nothing but ptr itself. the obvious place is right before the memory you handed out.<br>typedef struct {<br>size_t size;<br>} Header;
so instead of just handing back raw memory, alloc() now does this:<br>[ Header ][ User Memory ]<br>returned to caller
and free(ptr) walks backward:<br>Header *h = (Header *)ptr - 1;<br>// now h->size tells us how big this block was
this is the naive approach. there is more ;)<br>naive header vs alignment<br>not every type wants to live at any address. they call it alignment .<br>char doesn’t care where it sits in memory. (alignment = 0)<br>double does most platforms require it to start at an address that’s a multiple of 8 (alignment = 8)<br>…and many more for different types.<br>there is no strict rule that stops the program from compiling when using a different alignment. it comes where the performance may hit or crashes happen unexpectedly because of it.<br>so alloc() doesn’t just need to hand back a pointer. it needs to hand back a pointer that’s correctly aligned for whatever type the caller is about to store there.<br>which means there might be a gap between where the header ends and where the user pointer actually needs to start, and that gap is a different size every single time, depending on what alignment was requested.<br>the gap is what they call a padding .<br>[ Header ][ ...variable padding... ][ User Memory ]
and this is exactly where header = (Header *)ptr - 1 falls apart. that subtraction assumes a fixed distance from ptr back to the header. but the padding isn’t fixed, it changes per-allocation.<br>free() has no way of knowing how much padding was inserted for this specific pointer, so it can’t reliably walk backward to find the header anymore.<br>the back pointer<br>if the distance to the header isn’t fixed, don’t rely on distance. store a pointer to the header instead, at a position that is always fixed.<br>[ Header ][ ...variable padding... ][ Back Pointer ][ User Memory ]<br>^ always exactly sizeof(void*)<br>bytes before User Memory,<br>no matter how much padding<br>came before it
now free() doesn’t need to know or recompute any padding. it just does:<br>Header *h = *(Header **)((uint8_t *)ptr - sizeof(void*));
that slot is always sizeof(void*) bytes before the user pointer, every single time, regardless of alignment.<br>the header can live wherever it wants, as far away...