Translations: English
One thing that all programs on your computer have in common is a need for memory. Programs need to be loaded from your hard drive into memory before they can be run. While running, the majority of what programs do is load values from memory, do some computation on them, and then store the result back in memory.
In this post I'm going to introduce you to the basics of memory allocation. Allocators exist because it's not enough to have memory available, you need to use it effectively. We will visually explore how simple allocators work. We'll see some of the problems that they try to solve, and some of the techniques used to solve them. At the end of this post, you should have a good idea of the problems memory allocators are trying to solve.
# malloc and free
To understand the job of a memory allocator, it's essential to understand how
programs request and return memory.
If you have beginner-level familiarity with another language, e.g. JavaScript, Python, or C#, you should have no problem following along. You don't need to understand every word, as long as you get the overall idea. This is the only C code in the article, I promise.
#include <stdlib.h>
int main() {
void *ptr = malloc(4);
free(ptr);
return 0;
}
In the above program, we ask for 4 bytes of memory on line malloc(4), and store
the value returned in a variable called ptr. On line free(ptr).
These two functions are how almost all programs manage the memory they use.
Even when you're not writing C, the code that is executing your Java, Python,
Ruby, JavaScript, and so on make use of
# What is memory?
The smallest unit of memory that allocators work with is called a "byte." A byte can store any number between 0 and 255. You can think of memory as being a long sequence of bytes. We're going to represent this sequence as a grid of squares, with each square representing a byte of memory.
In the C code from before, malloc(4) allocates 4 bytes of memory. We're going
to represent memory that has been
Then free(ptr) tells the allocator we're done with that memory. It is returned
back to the pool of
Here's what 4
0x prefix to distinguish
them from decimal numbers. Move the
Here's our familiar grid of memory. Each byte is annotated with its address in
hexadecimal form. The
The examples we use in this article pretend that your computer only has a very small amount of memory, but in real life you have billions of bytes to work with. Real addresses are much larger than what we're using here, but the idea is exactly the same. Memory addresses are numbers that refer to a specific byte in memory.
# The simplest malloc
The "hello world" of
You'll notice no memory is
Believe it or not, this isn't a completely useless implementation. For programs
that use a known amount of memory, this can be a very efficient strategy. It's
extremely fast and extremely simple. As a general-purpose memory allocator,
though, we can't get away with having no
# The simplest general-purpose malloc
In order to
We're representing size=32. When our program starts, all of memory is marked as free. When
You caught me. One of the benefits of being a memory allocator is that you're in charge of memory. You could store your allocation/free list in a reserved area that's just for you. Or you could store it inline, in a few bytes immediately preceding each allocation. For now, assume we have reserved some unseen memory for ourselves and we're using it to store our allocation and free lists.
So what about
Our
We allocated 8 blocks of memory, each 4 bytes in size. Then we malloc(8), there are no items in our malloc(8) will fail.
To solve this, we need to do a bit more work. When we
Much better.
# Fragmentation
A perfectly coalesced free list doesn't solve all of our problems. The following example shows a longer sequence of allocations. Have a look at the state memory is in at the end.
We end this sequence with 6 of our 32 bytes free, but they're split into 2
blocks of 3 bytes. If we had to service a malloc(6), while we have enough free
memory in theory, we wouldn't be able to. This is called "fragmentation."
Sadly not. Remember earlier we talked about how the return value of
If we can't move allocations after creating them, we need to be more careful about where we put them to begin with.
One way to combat fragmentation is, confusingly, to overallocate. If we always allocate a minimum of 4 bytes, even when the request is for 1 byte, watch what happens. This is the exact same sequence of allocations as above.
Now we can service a malloc(6). It's worth keeping in mind that this is just
one example. Programs will call
No, that's a side-effect of overallocating. The visualisation shows "true"
memory use, whereas the free list is updated from the allocator's perspective.
So when the first
It's worth noting that this unused space that results from overallocation is another form of fragmentation. It's memory that cannot be used until the allocation that created it is freed. As a result, we wouldn't want to go too wild with overallocation. If our program only ever allocated 1 byte at a time, for example, we'd be wasting 75% of all memory.
Another way to combat fragmentation is to segment memory into a space for small
allocations and a space for big ones. In this next visualisation we start with
two free lists. The
Nice! This also reduces fragmentation. If we're strictly only allowing
allocations of 3 bytes or fewer in the first segment, though, then we can't
service that malloc(6). The trade-off here is that reserving a segment of
memory for smaller allocations gives you less memory to work with for bigger
ones.
Got me again. This implementation I've written will put small allocations in the
Allocators that split memory up based on the size of allocation are called "slab allocators." In practice they have many more size classes than the 2 in our example.
# A quick malloc puzzle
What happens if you malloc(0)? Have a think about this before playing with
the
This is using our
It turns out that what happens when you malloc(0) differs between
implementations. Some of them behave as above, allocating space they probably
didn't have to. Others will return what's called a "null pointer", a special
pointer that will crash your program if you try to read or write the memory it
points to. Others pick one specific location in memory and return that same
location for all calls to malloc(0), regardless how many times it is called.
Moral of the story? Don't malloc(0).
# Inline bookkeeping
Remember earlier on when you asked about where allocation list and free list information gets stored, and I gave an unsatisfying answer about how it's stored in some other area of memory we've reserved for ourselves?
This isn't the only way to do it. Lots of allocators store information right next to the blocks of memory they relate to. Have a look at this.
What we have here is memory with no allocations, but free list information
stored inline in that memory. Each block of memory, free or used, gets 3
additional bytes of bookkeeping information. If
address + 0 is thesize of the blockaddress + 1 is whether the block isfree (1) orused (2) address + 2 is where theusable memory startsaddress + 2 + size -- thesize of the block again
So in this above example, the byte at
It seems wasteful at first, but it is necessary if we want to do any form of coalescing. Let's take a look at an example.
Here we've allocated 4 bytes of memory. To do this, our
This has resulted in the creation of a used block (notice the 2 stored in the 2nd byte), and it has pushed start of the free block forward by 7 bytes. Let's do the same again and allocate another 4 bytes.
Next, let's malloc(4). The implementation of
How great is that? Simple, fast.
What if we wanted to free the 2nd block of used memory? We know that we want to coalesce to avoid fragmentation, but how do we do that? This is where the seemingly wasteful bookkeeping comes into play.
When we coalesce, we check to see the state of the blocks immediately before and
immediately after the block we're
Allocators that store bookkeeping information like this alongside allocations are called "boundary tag allocators."
Surprisingly, nothing truly prevents this. We rely heavily, as an industry, on
the correctness of code. You might have heard of "buffer overrun" or "use after
free" bugs before. These are when a program modifies memory past the end of an
allocated block, or accidentally uses a block of memory after
We're seeing a rise in popularity of "memory safe" languages, for example Rust. These languages invest a lot in making sure it's not possible to make these types of mistake in the first place. Exactly how they do that is outside of the scope of this article, but if this interests you I highly recommend giving Rust a try.
You might have also realised that calling
To get around this, some allocators inject "magic" values as part of the
bookkeeping information. They store, say, 0x55 at address + 2. This would
waste an extra byte of memory per allocation, but would allow them to know when
a mistake has been made. To reduce the impact of this, allocators often disable
this behaviour by default and allow you to enable it only when you're debugging.
# Playground
If you're keen to take your new found knowledge and try your hand at writing
your own allocators, you can try my allocator playground.
You'll be able to write JavaScript code that implements
the
Enjoyed this? Sign up to get notified of new posts.
# Conclusion
We've covered a lot in this post, and if it has left you yearning for more you
won't be disappointed. I've specifically avoided the topics of virtual memory,
brk vs mmap, the role of CPU caches, and the endless tricks real
Join the discussion on Hacker News!
Wanna tell me what you thought of the post? This form sends a notification directly to my phone.
# Acknowledgments
Special thanks to the following people:
- Chris Down for lending me his extensive knowledge of real-world memory allocators.
- Anton Verinov for lending me his extensive knowledge of the web, browser developer tools, and user experience.
- Blake Becker, Matt Kaspar, Krista Horn, Jason Peddle, and Josh W. Comeau for their insight and constructive reviews.