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)
}