Go Lang: Memory Management and Garbage Collection

While writing a program or application, one of the most important thing that we developers come across is, how our application/program going to behave from memory consumption point of view and we put in lot of effort in writing code that is efficient from memory consumption point of view and we do ensure that our application/program shouldn’t leak memory leading to unpleasant crash of our application. With this in mind we looked for someone (frameworks, languages etc.) that can take care of memory management on our behalf and allow us to focus more on codifying business logic.

With C, C++ its fully on developers to code for allocation/de-allocation of memory, with Java and others, came garbage collection. Go suggests that as a developer we should not be concerned about coding for the variable’s values placement in memory and hence not worry about freeing up the allocated space. Go takes away this memory management from developers and allows us to focus on implementing business.

Let start with where all the variables get placed in running process address space. The stack, the heap and the data segment are 3 sections where we get our process variables/functions placed.

  • Typically, a functions parameters and local variables are allocated on the stack.
  • Data that has longer life span than the scope it was created in finds place on heap.
  • Data segment, where global variables are stored. The data segment is defined at compile time and therefore does not grow and shrink at run time.

Stack grows downward and Heap grows upward within the process address space with “Guard Page” in between to avoid collision between these 2 memory spaces. Go routines scores over threads on this as:

  • As threads share the same address space, so for each thread, it must have its own stack.
  • A large amount of memory is reserved for each thread’s stack along with a guard page.
  • With the hope that threads stack will never hit guard page it gets large amount memory allocated.

The downside is that as the number of threads in our program increases, the amount of available address space is reduces.

Go run time schedules a large number of goroutines onto a small number of threads, but how is the stack managed for those goroutines? Instead of using guard pages, the Go compiler inserts a check as part of every function call to check if there is sufficient stack for the function to run. If there is not, the runtime can allocate more stack space. Because of this check, a goroutines can start with much smaller stack of as small as 8kb.

 

All right, and the good part is that its all managed by Go run time for us. The Go garbage collector manages the heap for us. Most of us would agree that GC cycle is a cost to our application and there are various implementations of GCs, some focusing on throughput (like Haskel’s GHC), some on latency. Go’s GC focuses on latency and it has improved it’s stop-the-world  GC pause time: from 300 ms (Go 1.4) to 40 ms (Go 1.5) to 3 ms (Go 1.6) to ~1 ms (Go 1.7) and making it even better with Go 1.8.

GO’s GC is concurrent meaning it interleaves with the program and the pause times become a scheduling problem. The scheduler can be configured to only run GC collections for short periods of time which is great for applications with low latency requirements.

Before we dive deeper into GC, lets look at whether we as developer has any way to take a peek into what makes Go decide which variable(s) of our program goes where. Escape Analysis of Go gives developer a look into which variable(s) gets placed where.

Escape analysis is used to determine whether an item can be allocated on the stack. It determines if an item created in a function (e.g., a local variable) can escape out of that function or to other goroutines.

  • If no references escape, the value may be safely stored on the stack.
  • Values stored on the stack do not need to be allocated or freed.

Lets look at few example:


package main

import (
 "fmt"
)

func getAge() *int {
 age := 34
 return &age
}
func main() {
 fmt.Println(*getAge())
}

To get what compiler thought about this program and chose to place which variable(s) where, run:

go build -gcflags=-m main.go

cap1.jpg

Clearly by returning pointer to age from getAge, we are making it live longer than the scope of the function it got created, hence compiler decided to place it on heap.

Lets take another example:


package main

import (
 "fmt"
)

const Width, Height = 640, 480

type Point struct {
 X, Y int
}

func MoveToCenter(c *Point) {
 c.X += Width / 2
 c.Y += Height / 2
}

func main() {
 p := new(Point)
 MoveToCenter(p)
 fmt.Println(p.X, p.Y)
}

 

 

The output of the go build -gcflags=-m main.go is:

cap2.JPG

Surprise surprise! why ‘p’ that has been created with ‘new’ is not escaping to heap? During code analysis compiler is sure that variable p’s life span is well within the life span of its scope and its not escaping it hence it efficiently places ‘p’ on stack, reducing load on GC and improvising the performance of this program.

Do we also see that as part of its code optimization compiler is in lookout of functions that it can inline and reduce function call overhead? Yes we do see, it has in-lined getAge and MoveToCenter, as it found that the cost of function call is more than the work this function is doing (smaller one). We may not agree to the compiler’s choice each time at in-lining the function front and can choose to drop it and go with function call overhead. We can do that with another gcflags option ‘-l’ (letter L in small case) with:

go build -gcflags=”-m -l” main.go

cap3

What do we get out of escape analysis? It gives us an opportunity to analyse our code from memory stand point and take corrective actions by moving as much variables as possible to stack, reducing GC overhead on our application, also improve our function definition to make it qualify for inlining (storage is not that big of challenge these days!), in case we are fine with an increased binary size. Function inlining also helps in identifying dead code and eliminating them.

Armed with escaping analysis results and after working on it optimize our code with an aim to reduce as much overhead on GC possible, can we still participate in controlling GC? Yes, we can turn it completely off (when we are 200% sure that we don’t need GC for our program), we can tweak its execution cycle.

From Go lang run time package documentation:

The GOGC variable sets the initial garbage collection target percentage. A collection is triggered when the ratio of freshly allocated data to live data remaining after the previous collection reaches this percentage. The default is GOGC=100. Setting GOGC=off disables the garbage collector entirely.

GOGC=off ./main.exe

With all these information and tooling at our disposal, our code may still leak memory, below are few quick and easier ones to keep eyes on to avoid memory leaks:

  • Writing defer calls as close, avoid defer in conditions or loop, please see this:

package main

import (
"io/ioutil"
"log"
"net/http"
_ "net/http/pprof"
)

var done chan bool

func main() {

http.HandleFunc("/", handleRequest)

log.Fatalln(http.ListenAndServe(":8080", http.DefaultServeMux))

}
func handleRequest(w http.ResponseWriter, req *http.Request) {
// make a new channel
done = make(chan bool)
for i := 0; i < 3; i++ {
go doSomething()
}
// wait for all the goroutines to finish, and return
for i := 0; i < 3; i++ {
<-done
}
w.Write([]byte("Done processing"))
}

func doSomething() {
// signal we are done doing something
defer func() { done <- true }()
// perform a web request
resp, err := http.Get("http://localhost:50055/debug/requests")
if err != nil {
log.Fatal(err)
}
//defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
}
}

This code leaks resp.Body, because resp.Body.Close() is not always placed for execution, just by un-commenting line#40 and commenting line# 46, this issue solves.

  • Calling goroutines in a loop, that blocks

for i := range whereToSend{
go func() {
whereToSend[i] <- value
}()
}

It works fine unless the goroutine that was receiving from the channel stopped receiving for some reason resulting in leaking memory. This could be done like:


for i := range whereToSend{
select {
case whereToSend[i] <- value:
default:
}
}

Happy Coding!!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s