Writing a Simple Garbage Collector in C (2014)
maplant.com
maplant.com
When I migrated to my own personal domain, I decided I would update the article to address the comments on that thread, and I think the end result was much better (but alas, I never had a reason to re-submit the article here).
Obviously I do not recommend using a GC like this in general, but as a learning resource for how memory works and some of the more obscure parts of executables, I think it works pretty well! The compliments I've received from this article have been pretty incredible. Seeing it here, uploaded independently, really warms my heart.
Oh, and this article is so old that I'm now a Rust guy. Crazy how that works!
A couple of other classic articles that should be mentioned:
"Accurate garbage collection in an uncooperative environment" by Fergus Henderson - https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&d...
"Accurate Garbage Collection in Uncooperative Environments Revisited" - http://www.filpizlo.com/papers/baker-ccpe09-accurate.pdf
For comparison, this is the malloc/free implementation of the original UNIX. Also nice and tight C code.
https://warsus.github.io/lions-/all.html#line2522
Although this isn’t very efficient for today’s memory sizes.
The most common patterns I often see are a "context" object of some kind (typically used by libraries) which handles all memory management internally and must be passed to every API call. So you only ever allocate and free that one object. (Internally they might be doing all sorts of crazy things, I've even seen a basic tracing GC!)
Applications typically use some combination of bump allocators (aka arenas) and pools. You put temporary allocations in a dedicated temp arena and clear it out when appropriate for the application (i.e. every frame)
void dwim(unsigned count) {
struct Something _buffer[10];
struct Something* buffer;
if (count < sizeof(_buffer) / sizeof(_buffer[0]))
buffer = _buffer;
else {
buffer = (struct Something*) malloc(count * sizeof(struct Something));
}
doSomething(buffer, count);
if (buffer != _buffer)
free(buffer);
}
It used to be really common to write that without considering if it was a necessary optimization or not - nowadays I would just malloc up front.One of the best of the latter is arena management. (https://en.wikipedia.org/wiki/Region-based_memory_management)
Ryan Fleury has a great write up on the subject:
https://www.rfleury.com/p/untangling-lifetimes-the-arena-all...
Yes. It's common to write OOP style C by using an opaque pointer in a header file and handle all the allocation and deallocation within the code file. The header file will export a "constructor" and "destructor" function. The constructor and destructor are still called manually, but this method properly encapsulates state to being visible only within a single code file, which prevents some accidental misuses of a type. The destructor should have a free for every allocation in the constructor, but done in reverse order. If you follow this pattern consistently, then `malloc` will only ever appear inside a constructor and `free` will only appear in a destructor. All other object allocation and deallocation is done via the relevant constructor/destructor.
.h file:
typedef struct my_type_t my_type;
my_type* my_type_alloc (type1, size_t);
void my_type_free (my_type*);
.c file: struct my_type_t
{
type1 member1;
type2* member2;
};
my_type* my_type_alloc (type1 m1_copy, size_t m2_size);
{
my_type result* = (my_type*) malloc (sizeof (my_type));
result->member1 = m1_copy;
result->member2 = type2_alloc (m2_size);
return result;
}
void my_type_free (my_type* value)
{
type2_free (value->member2);
free (value);
}
Another technique is to make use of GCC `constructor` and `destructor` attributes, which are called before `main` and after `main` loses scope (or `exit()` is called). For example, you might have a static container type and only expose methods `add`, `remove` and `get`, then all allocation and deallocation happens solely within the code file and consumers of this API don't need to concern themselves with allocating and deallocating. This is only really useful for singleton-like (process global) data structures, but you could perhaps utilize these for implementing a GC, since an allocator is usually global to the process. (Eg, the `GC_init` function in the article could be given the constructor attribute so that the programmer would not need to manually call it)..h file:
void container_add (obj value);
void container_remove (obj value);
obj container_get (size_t index);
.c file: struct container_t
{
obj* items;
size_t num_items;
size_t capacity;
}
static container_t* c;
__attribute__((constructor))
void container_initialize(void)
{
c = malloc (sizeof (struct container_t));
c->num_items = 0;
c->capacity = DEFAULT_NUM_ITEMS;
c->items = malloc(DEFAULT_NUM_ITEMS * sizeof (obj));
}
__attribute__((destructor))
void container_uninitialize(void)
{
free (c->items);
free (c);
}
void container_resize (size_t new_size)
{
obj* tmp = c->items;
c->capacity = new_size;
c->items = malloc (new_size * sizeof (obj));
memcpy (c->items, tmp, c->num_items * sizeof (obj));
free (tmp);
}
void container_add (obj value) {
if (c->num_items == c->capacity) container_resize (c->capacity * 2);
...
}
void container_remove (obj value) { ... }
obj container_get (size_t index) {
return c->items[index];
}Writing a Simple Garbage Collector in C - https://news.ycombinator.com/item?id=21794327 - Dec 2019 (23 comments)
Writing a Simple Garbage Collector in C - https://news.ycombinator.com/item?id=8222487 - Aug 2014 (47 comments)
_The Garbage Collection Handbook, 2nd Edition_ - https://news.ycombinator.com/item?id=35492307
And for a proper compacting GC there are many options, best being ravenbrook's MPS, but these require lot of source code adaptions.
Brave.
Did you know Ruby has a garbage collector not unlike this one? People are running it in production right now.
> a toy GC
It does its job. These algorithms have been working for decades now.
> without threading support
It can just stop the whole world.
> without register scanning
Add some inline assembly to spill all the registers. I did this on my own language implementation not even a month ago. If this was a real project I'd submit a patch.
The author even acknowledged this limitation and hinted at how to solve it, he just didn't want to get into those gritty details in his article. I'm happy to demonstrate if you want.
> Imprecise also. How do you seperate large ints from pointers?
You don't. You collect conservatively. It will have false positives but not false negatives. In other words, it will mistake some ints for pointers but it won't collect memory that's in use. Correct if suboptimal behavior.