Garbage Collector Optimization

Go’s garbage collector has evolved significantly, but understanding how to tune and work with it is essential for high-performance applications.

Understanding Go’s GC Algorithm

Go uses a concurrent, tri-color mark-and-sweep garbage collector:

package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	// Print initial GC stats
	printGCStats("Initial")
	
	// Allocate some memory
	data := make([]*[1024]byte, 1000)
	for i := 0; i < 1000; i++ {
		data[i] = &[1024]byte{}
	}
	
	// Print GC stats after allocation
	printGCStats("After allocation")
	
	// Force a GC
	runtime.GC()
	
	// Print GC stats after forced GC
	printGCStats("After forced GC")
	
	// Create some garbage
	for i := 0; i < 500; i++ {
		data[i] = nil
	}
	
	// Force another GC
	runtime.GC()
	
	// Print final GC stats
	printGCStats("After creating garbage")
}

func printGCStats(label string) {
	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)
	
	fmt.Printf("--- %s ---\n", label)
	fmt.Printf("HeapAlloc: %d KB\n", stats.HeapAlloc/1024)
	fmt.Printf("HeapSys: %d KB\n", stats.HeapSys/1024)
	fmt.Printf("HeapObjects: %d\n", stats.HeapObjects)
	fmt.Printf("GC cycles: %d\n", stats.NumGC)
	fmt.Printf("Total GC pause: %v\n", time.Duration(stats.PauseTotalNs))
	fmt.Printf("Last GC pause: %v\n\n", time.Duration(stats.PauseNs[(stats.NumGC+255)%256]))
}

Tuning GC Parameters

Go allows tuning the garbage collector through environment variables and runtime functions:

package main

import (
	"fmt"
	"os"
	"runtime"
	"runtime/debug"
	"strconv"
	"time"
)

func allocateAndMeasure(label string, allocSize int, iterations int) {
	// Clear previous garbage
	runtime.GC()
	
	var stats runtime.MemStats
	runtime.ReadMemStats(&stats)
	startHeap := stats.HeapAlloc
	startGC := stats.NumGC
	
	// Record start time
	startTime := time.Now()
	
	// Allocate memory
	var data [][]*int
	for i := 0; i < iterations; i++ {
		block := make([]*int, allocSize)
		for j := 0; j < allocSize; j++ {
			val := new(int)
			*val = i*allocSize + j
			block[j] = val
		}
		data = append(data, block)
		
		// Keep only the last 5 blocks to create GC pressure
		if len(data) > 5 {
			data = data[1:]
		}
	}
	
	// Record end time
	duration := time.Since(startTime)
	
	// Get final stats
	runtime.ReadMemStats(&stats)
	allocatedHeap := stats.HeapAlloc - startHeap
	gcCount := stats.NumGC - startGC
	
	fmt.Printf("--- %s ---\n", label)
	fmt.Printf("Duration: %v\n", duration)
	fmt.Printf("Allocated: %d KB\n", allocatedHeap/1024)
	fmt.Printf("GC runs: %d\n", gcCount)
	fmt.Printf("GC percentage: %.2f%%\n\n", float64(stats.GCCPUFraction)*100)
}

func main() {
	// Print initial GC settings
	fmt.Printf("Initial GOGC: %s\n", os.Getenv("GOGC"))
	fmt.Printf("Initial GCPercent: %d%%\n\n", debug.SetGCPercent(-1))
	debug.SetGCPercent(100) // Reset to default
	
	// Test with default GC settings (GOGC=100)
	allocateAndMeasure("Default GC (GOGC=100)", 10000, 100)
	
	// Test with more aggressive GC (GOGC=50)
	debug.SetGCPercent(50)
	allocateAndMeasure("Aggressive GC (GOGC=50)", 10000, 100)
	
	// Test with less aggressive GC (GOGC=200)
	debug.SetGCPercent(200)
	allocateAndMeasure("Less Aggressive GC (GOGC=200)", 10000, 100)
	
	// Test with GC disabled (GOGC=off)
	debug.SetGCPercent(-1)
	allocateAndMeasure("GC Disabled (GOGC=off)", 10000, 100)
	
	// Reset GC to default
	debug.SetGCPercent(100)
	
	// Demonstrate setting GOGC via environment variable
	fmt.Println("Setting GOGC=150 via environment variable")
	os.Setenv("GOGC", "150")
	
	// Need to restart the program for this to take effect
	// This is just for demonstration
	gogcVal, _ := strconv.Atoi(os.Getenv("GOGC"))
	fmt.Printf("GOGC environment variable: %d\n", gogcVal)
}