flak rss random

go garbage collector and liveness

Depending on language, compiler, and runtime, sometimes the garbage collector needs a few hints from the programmer. You know you’re done with an object, but to the GC, if a variable appears live, it can’t be collected. Sometimes the problem really is programmer error, as objects continue to collect in a container that’s never inspected. Other times the variable will be overwritten soon enough, but does it help to overwrite it sooner?

A trivial example.

function homework(problem)
    A = bigarray(problem)
    Aprime = invert(A)
    Answer = calculate(Aprime)
    if verify(Answer)
        print("I done it")

In this pseudocode, the variables A and Aprime are effectively dead by the time we call verify. However, if they are still on the stack, the garbage collector can’t recycle them. If verify requires a lot of memory, we may have some trouble. Let’s try managing things a little more manually.

function homework(problem)
    A = bigarray(problem)
    Aprime = invert(A)
    A = null
    Answer = calculate(Aprime)
    Aprime = null
    if verify(Answer)
        print("I done it")

In the revised version, we null out the extra arrays. When the garbage collector examines the stack, it won’t find any references and they can be safely collected.

Now whether one definitely needs to do this is a combination of rumor and superstition.

I was curious about go, and I’m not sure what search terms to use or where I’d look in the documentation, so I wrote a little test.

package main

import "time"

func bubble(depth int) int {
        if depth > 9 {
                return 0
        }
        mem := make([]byte, 127654321)
        sum := 0
        for _, b := range(mem) {
                sum += int(b)
        }
        time.Sleep(2 * time.Second)
        bb := bubble(depth + 1)
        return sum + bb
}

func main() {
        bubble(0)
}

This program would appear to allocate about 1GB of memory, growing over time. But when I ran it, memory peaked at about 130MB. Did I have a bug? I tried adding a bit more code after the recursive call.

package main

import "time"

func bubble(depth int) int {
        if depth > 9 {
                return 0
        }
        mem := make([]byte, 127654321)
        sum := 0
        for _, b := range(mem) {
                sum += int(b)
        }
        time.Sleep(2 * time.Second)
        bb := bubble(depth + 1)
        for _, b := range(mem) {
                sum += int(b)
        }
        return sum + bb
}

func main() {
        bubble(0)
}

Now this test definitely uses lots of memory. The go compiler is smart enough to know that mem is dead before the recursive call. I was expecting I would need to add “mem = nil” in there somewhere, but nope.

In what circumstances does this work? When does it not work? Still not sure. But at least it looks like it shouldn’t be a problem one need worry about.

Some more links on the subject, with even more fun test cases. SSA and liveness. Issue 15277 - must flush dead arguments. Issue 13347 spell out rules for finalizers.

Posted 02 Jan 2017 15:18 by tedu Updated: 03 Aug 2017 15:25
Tagged: go programming