// HACKER NEWS — CYBERSECURITY
What garbage collection actually costs
Every computer program needs memory. Memory is finite, so a long running program
must borrow memory from the operating system when it needs it and release it
once it is no longer needed.
The interesting part is who reclaims that memory, and when. Some piece of code
has to figure out that a piece of memory is no longer needed and release it, and
figuring that out is not trivial. A value might be passed to another function,
stored somewhere longer-lived, or shared across threads, and it stays needed as
long as anything still refers to it. If it is reclaimed early, we get memory
corruption. If it is reclaimed too late, we get memory leaks.
There are two paradigms for memory management, each optimizing for different things.
The first paradigm is to let the language runtime do it. A program allocates
memory when it needs it, uses it as long as required, and eventually stops
referring to it. A garbage collector figures out what is no longer reachable and
reclaims it. Go, Java and a lot of other languages in wide use belong to this
category. You give up deciding when memory is freed, but in exchange you cannot
free it too early, free it twice, or forget to free it at all. For most software
this is a very good compromise. It reduces the cognitive overhead of memory
management and you can stay focused on the actual problem. The leaks that come
from forgetting to free something go away entirely.
The second paradigm, which must be very evident at this point, is to keep the
decision to yourself. In C, you allocate and free by hand, and you own every bug
that comes as a result. In Rust you do not write the frees, but you do not hand
the decision to a runtime either. The compiler works out at build time where
each value’s life ends, reclaims it there, and refuses to build the program if
it cannot prove that this is safe. So you still get control over memory and a
tighter footprint, but the effort shifts. In C you pay for it by debugging
corruption. In Rust you pay for it by arranging your program in a way the
compiler can verify.
Sitting in between is reference counting, which is what Swift and Python do. It
is really a variant of the first answer rather than a third paradigm, and it is
seldom enough on its own. Counts cannot see cycles, so a language has to deal
with them some other way. Python bolts on a tracing collector that hunts for
cycles. Swift does not, and instead pushes the problem back to you through
weak and unowned annotations. Reference counting also has its own running
cost, paid on every copy of a pointer you make and every time you drop one.
Which one should you choose? If garbage collection was free, all of us would
choose a runtime that manages memory on its own. But it is not free, hence we
discuss the performance penalty of GC and whether it matters.
When a program needs memory it comes from one of two places, the stack or the
heap. Stack memory costs the collector nothing. It grows and shrinks as
functions are called and return, and the machine just moves a pointer. When the
function returns the value is gone, nothing has to reclaim it. Stacks are not
entirely invisible to the collector, since it has to scan them as roots to find
where the live objects start, but it never has to free anything there.
A value ends up on the heap for one of two reasons. Either it needs to outlive
the function that created it, because you returned a reference to it, or stored
it somewhere longer-lived. Or its size is not known up front and it can grow.
For example, a slice you keep appending to, a buffer sized from user input. Heap
allocation is the class of memory which GC monitors and reclaims, and it is what
factors into GC costs. We will discuss this in the next section.
To work out what collection costs you, there are two questions to answer: how
often the collector runs, and what a single run costs.