Memory Profiling and Optimization
Memory usage is often a critical factor in Go application performance, especially as it relates to garbage collection overhead.
Memory Profiling Techniques
Here’s how to effectively profile memory usage in Go applications:
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime"
"runtime/pprof"
)
// Function that allocates memory
func allocateMemory() {
// Allocate a large slice that will stay in memory
data := make([]byte, 100*1024*1024) // 100 MB
// Do something with the data to prevent compiler optimizations
for i := range data {
data[i] = byte(i % 256)
}
// Keep a reference to prevent garbage collection
// In a real app, this might be stored in a global variable or cache
keepReference(data)
}
// Prevent compiler from optimizing away our allocations
var reference []byte
func keepReference(data []byte) {
reference = data
}
// Function that leaks memory
func leakMemory() {
// This function simulates a memory leak by storing data in a global slice
// without ever cleaning it up
for i := 0; i < 1000; i++ {
// Each iteration leaks 1MB
leak := make([]byte, 1024*1024)
for j := range leak {
leak[j] = byte(j % 256)
}
leakedData = append(leakedData, leak)
}
}
// Global variable to simulate a memory leak
var leakedData [][]byte
func main() {
// Parse command line flags
memprofile := flag.String("memprofile", "", "write memory profile to file")
leakMode := flag.Bool("leak", false, "demonstrate memory leak")
flag.Parse()
// Allocate memory
allocateMemory()
// Optionally demonstrate a memory leak
if *leakMode {
fmt.Println("Demonstrating memory leak...")
leakMemory()
}
// Force garbage collection to get accurate memory stats
runtime.GC()
// Print memory statistics
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc))
fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc))
fmt.Printf("\tSys = %v MiB", bToMb(m.Sys))
fmt.Printf("\tNumGC = %v\n", m.NumGC)
// Write memory profile if requested
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close()
// Write memory profile
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("could not write memory profile: ", err)
}
fmt.Printf("Memory profile written to %s\n", *memprofile)
fmt.Println("Analyze with: go tool pprof -http=:8080", *memprofile)
}
// Show how to use net/http/pprof for continuous memory profiling
fmt.Println("\nFor continuous memory profiling in a web server:")
fmt.Println("1. Import _ \"net/http/pprof\"")
fmt.Println("2. Access heap profile at: http://localhost:8080/debug/pprof/heap")
fmt.Println("3. Download and analyze with: go tool pprof http://localhost:8080/debug/pprof/heap")
// Show how to use testing package for benchmark memory profiling
fmt.Println("\nFor memory profiling during benchmarks:")
fmt.Println("go test -bench=. -memprofile=mem.prof ./...")
fmt.Println("Then analyze with: go tool pprof -http=:8080 mem.prof")
}
// Convert bytes to megabytes
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
Detecting and Fixing Memory Leaks
Memory leaks in Go often manifest as growing heap usage over time. Here’s how to detect and fix them:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof" // Import for side effects
"os"
"runtime"
"time"
)
// Cache that doesn't clean up old entries (leak)
type LeakyCache struct {
data map[string][]byte
// Missing: expiration mechanism
}
func NewLeakyCache() *LeakyCache {
return &LeakyCache{
data: make(map[string][]byte),
}
}
func (c *LeakyCache) Set(key string, value []byte) {
c.data[key] = value
}
func (c *LeakyCache) Get(key string) []byte {
return c.data[key]
}
// Fixed cache with expiration
type FixedCache struct {
data map[string]cacheEntry
maxEntries int
}
type cacheEntry struct {
value []byte
expiration time.Time
}
func NewFixedCache(maxEntries int) *FixedCache {
cache := &FixedCache{
data: make(map[string]cacheEntry),
maxEntries: maxEntries,
}
// Start cleanup goroutine
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
cache.cleanup()
}
}()
return cache
}
func (c *FixedCache) Set(key string, value []byte, ttl time.Duration) {
// Enforce maximum entries
if len(c.data) >= c.maxEntries {
c.evictOldest()
}
c.data[key] = cacheEntry{
value: value,
expiration: time.Now().Add(ttl),
}
}
func (c *FixedCache) Get(key string) ([]byte, bool) {
entry, found := c.data[key]
if !found {
return nil, false
}
// Check if expired
if time.Now().After(entry.expiration) {
delete(c.data, key)
return nil, false
}
return entry.value, true
}
func (c *FixedCache) cleanup() {
now := time.Now()
for key, entry := range c.data {
if now.After(entry.expiration) {
delete(c.data, key)
}
}
}
func (c *FixedCache) evictOldest() {
var oldestKey string
var oldestTime time.Time
// Find the oldest entry
first := true
for key, entry := range c.data {
if first || entry.expiration.Before(oldestTime) {
oldestKey = key
oldestTime = entry.expiration
first = false
}
}
// Remove oldest entry
if oldestKey != "" {
delete(c.data, oldestKey)
}
}
// Simulate a memory leak
func simulateMemoryLeak() {
// Create a leaky cache
cache := NewLeakyCache()
// Start HTTP server for pprof
go func() {
fmt.Println("Starting pprof server on :8080")
http.ListenAndServe(":8080", nil)
}()
// Continuously add data to the cache without cleanup
for i := 0; ; i++ {
key := fmt.Sprintf("key-%d", i)
value := make([]byte, 1024*1024) // 1MB per entry
cache.Set(key, value)
// Print memory stats every 100 iterations
if i%100 == 0 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Iteration %d: Alloc = %v MiB, Sys = %v MiB\n",
i, bToMb(m.Alloc), bToMb(m.Sys))
time.Sleep(100 * time.Millisecond)
}
// Exit after some iterations in this example
if i >= 1000 {
break
}
}
}
// Demonstrate fixed cache
func demonstrateFixedCache() {
// Create a fixed cache with expiration
cache := NewFixedCache(100) // Max 100 entries
// Add data with expiration
for i := 0; i < 200; i++ {
key := fmt.Sprintf("key-%d", i)
value := make([]byte, 1024*1024) // 1MB per entry
cache.Set(key, value, 1*time.Minute)
// Print memory stats every 50 iterations
if i%50 == 0 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Iteration %d: Alloc = %v MiB, Sys = %v MiB, Entries: %d\n",
i, bToMb(m.Alloc), bToMb(m.Sys), len(cache.data))
time.Sleep(100 * time.Millisecond)
}
}
// Force GC to see the effect
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("After GC: Alloc = %v MiB, Sys = %v MiB, Entries: %d\n",
bToMb(m.Alloc), bToMb(m.Sys), len(cache.data))
}
func main() {
if len(os.Args) > 1 && os.Args[1] == "leak" {
fmt.Println("Simulating memory leak...")
simulateMemoryLeak()
} else {
fmt.Println("Demonstrating fixed cache...")
demonstrateFixedCache()
}
fmt.Println("\nTo analyze memory usage:")
fmt.Println("1. Run with leak simulation: go run main.go leak")
fmt.Println("2. In another terminal: go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap")
}
Optimizing for Garbage Collection
Understanding and optimizing for Go’s garbage collector can significantly improve application performance:
package main
import (
"fmt"
"runtime"
"runtime/debug"
"time"
)
// Function that generates a lot of garbage
func generateGarbage() {
for i := 0; i < 10; i++ {
// Allocate 100MB
_ = make([]byte, 100*1024*1024)
}
}
// Function that demonstrates GC tuning
func demonstrateGCTuning() {
// Print initial GC settings
fmt.Println("Default GC settings:")
printGCStats()
// Run with default settings
fmt.Println("\nRunning with default GC settings...")
measureGCPause(func() {
generateGarbage()
})
// Tune GC to be less aggressive (higher memory usage, fewer GC cycles)
fmt.Println("\nSetting higher GC percentage (less frequent GC)...")
debug.SetGCPercent(500) // Default is 100
printGCStats()
// Run with tuned settings
fmt.Println("\nRunning with tuned GC settings...")
measureGCPause(func() {
generateGarbage()
})
// Reset GC to default
debug.SetGCPercent(100)
// Demonstrate manual GC control
fmt.Println("\nDemonstrating manual GC control...")
// Disable GC temporarily
fmt.Println("Disabling GC...")
debug.SetGCPercent(-1)
// Allocate memory without GC
fmt.Println("Allocating memory with GC disabled...")
for i := 0; i < 5; i++ {
_ = make([]byte, 100*1024*1024)
printMemStats()
time.Sleep(100 * time.Millisecond)
}
// Manually trigger GC
fmt.Println("\nManually triggering GC...")
runtime.GC()
printMemStats()
// Re-enable GC
fmt.Println("\nRe-enabling GC...")
debug.SetGCPercent(100)
printGCStats()
}
// Function to measure GC pause times
func measureGCPause(fn func()) {
// Get initial GC stats
var statsBefore runtime.MemStats
runtime.ReadMemStats(&statsBefore)
numGCBefore := statsBefore.NumGC
// Run the function
start := time.Now()
fn()
elapsed := time.Since(start)
// Force a GC to get accurate stats
runtime.GC()
// Get GC stats after
var statsAfter runtime.MemStats
runtime.ReadMemStats(&statsAfter)
// Calculate GC stats
numGC := statsAfter.NumGC - numGCBefore
totalPause := time.Duration(0)
// Calculate total pause time
// Note: PauseNs is a circular buffer of recent GC pause times
for i := numGCBefore; i < statsAfter.NumGC; i++ {
idx := i % uint32(len(statsAfter.PauseNs))
totalPause += time.Duration(statsAfter.PauseNs[idx])
}
// Print results
fmt.Printf("Execution time: %v\n", elapsed)
fmt.Printf("Number of GCs: %d\n", numGC)
if numGC > 0 {
fmt.Printf("Total GC pause: %v\n", totalPause)
fmt.Printf("Average GC pause: %v\n", totalPause/time.Duration(numGC))
fmt.Printf("GC pause percentage: %.2f%%\n",
float64(totalPause)/float64(elapsed)*100)
}
}
// Print current GC settings
func printGCStats() {
fmt.Printf("GC Percentage: %d%%\n", debug.SetGCPercent(debug.SetGCPercent(-1)))
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Next GC target: %v MiB\n", bToMb(m.NextGC))
}
// Print memory statistics
func printMemStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc: %v MiB, Sys: %v MiB, NumGC: %v\n",
bToMb(m.Alloc), bToMb(m.Sys), m.NumGC)
}
func main() {
// Set max memory to show GC behavior more clearly
debug.SetMemoryLimit(1024 * 1024 * 1024) // 1GB
// Demonstrate GC tuning
demonstrateGCTuning()
fmt.Println("\nGC Tuning Best Practices:")
fmt.Println("1. Monitor GC frequency and pause times in production")
fmt.Println("2. Use GOGC environment variable for system-wide tuning")
fmt.Println("3. Consider debug.SetGCPercent for application-specific tuning")
fmt.Println("4. For latency-sensitive applications, consider increasing GOGC")
fmt.Println("5. For memory-constrained environments, consider decreasing GOGC")
}