drio

Mechanical sympathy and the golang garbage collector

Golang is a garbage collected programming language. This means that the runtime takes care of the garbage. By garbage we mean regions of memory that are marked as being used but do not have any pointer referencing them. At that point, we can reuse that memory to store other data. It is important to notice that failing to collect the garbage will result in our program eventually running out of memory. The OS will kill the process at that point.

The heap and the stack

Programming languages rely on two data structures to keep track of memory usage: the heap and the stack. The stack holds a consecutive block of memory. Every function call in a thread shares the same stack. The process of allocating memory in the stack is fast and simple. It is a matter of moving "up or down" the location of the stack pointer. When we make a function call, the runtime creates a new stack frame. Local variables and the parameters or the function live in that new stack frame. Each new variables moves the pointer by the size of the value. When we finish the execution of the function, we clear up the memory allocated for that function (the stack frame) and we copy the return values the previous stack frame.

In order to be able to store data in the stack. You have to know exactly how much space it requires at compile time.

But what happens when you don't know how much space you are going to need for your data? That's where the heap and the garbabe collector come in. The garbage collector manages the heap.

When we cannot allocated memory in the stack we say that the data the pointer points to escapes the stack.

Depending on the garbage collector algorithm, we may have instances where we have data that could have been stored in the stack but escapes to the heap.

What's wrong with storing data in the heap?

A couple of things.

  1. The process of garbage collecting takes cpu cycles away from our program. Instead of computing "useful" things, the cpu is burning cycles managing memory.

  2. RAM may be random access but it is still faster to access memory locations sequentially. Jumping around requires more time. A slice of structs in Go has all of the data laid out sequentially in memory. This makes it fast to load and fast to process. A slice of pointers to structs (or structs whose fields are pointers) has its data scattered across RAM, making it far slower to read and process.

We can place garbage collection algorithms in two categories depending what they favor:

  1. High throughput (find the most garbage in one scan).
  2. Lower latency (finish the garbage scan as quickly as possible).

High throughput will lead to better memory usage at the cost of more cpu cycles. Lower latency saves cpu cycles but may not clean as much garbage as algorithms in the other category.

Go's algorithm favors low latency. Each garbage collection cycle is designed to take less than 500 microseconds.

The practical lesson here is: think carefully the data structures you pick for your program. Is the data going to be in the heap or the stack? Prefer the stack. If your data escapes the stack and lands in the heap, make sure you keep an eye on your pointers so the garbage collector can clean the garbage. A lot of garbage not only may lead to running out of memory but also makes the garbage collector algorithm burn more cpu cycles, cycles we could use for our computations. And this is why the title of the post. The term mechanical sympathy comes the racing domain: a driver that knows the mechanical details of a car will be able to get more out of the car and go faster. We can use that in the context of software development. That is what Martin Thompson started doing in 2011.