正在加载,请稍候…

Go Performance Profiling: pprof, Benchmarks, and Memory Optimization

Learn to profile Go applications using pprof for CPU and memory analysis, write effective benchmarks, understand escape analysis, leverage sync.Pool, reduce allocations, and apply systematic performance optimization.

Go Performance Profiling: pprof, Benchmarks, and Memory Optimization

Performance optimization without measurement is guesswork. Go ships with a world-class profiling ecosystem—pprof, built-in benchmarks, execution tracing—that makes systematic optimization approachable. This guide walks through the complete workflow from identifying bottlenecks to validating improvements.

Setting Up pprof

Go's net/http/pprof package exposes profiling endpoints over HTTP, making it trivial to profile production services:

package main

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

func main() {
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()
    runServer()
}

For non-HTTP applications, use programmatic profiling:

func main() {
    cpuFile, _ := os.Create("cpu.prof")
    defer cpuFile.Close()

    pprof.StartCPUProfile(cpuFile)
    defer pprof.StopCPUProfile()

    doWork()

    memFile, _ := os.Create("mem.prof")
    defer memFile.Close()
    pprof.WriteHeapProfile(memFile)
}

Collecting Profiles

# CPU profile: 30 seconds of sampling
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Heap (memory) profile
go tool pprof http://localhost:6060/debug/pprof/heap

# Goroutine dump
go tool pprof http://localhost:6060/debug/pprof/goroutine

# Execution trace
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.out

Analyzing with pprof Interactive Shell

$ go tool pprof cpu.prof
(pprof) top10           # show top 10 functions by CPU time
(pprof) top10 -cum      # cumulative time (including callees)
(pprof) list processItem # show annotated source
(pprof) web             # open flamegraph in browser
(pprof) png > cpu.png   # save graph as image

Reading pprof output:

  • flat: time spent in the function itself
  • flat%: percentage of total time
  • cum: time in function + its callees
  • cum%: cumulative percentage including callees

Writing Effective Benchmarks

Go's testing package includes a benchmarking framework:

package stringutil_test

import (
    "strings"
    "testing"
)

func BenchmarkStringConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "x"
        }
        _ = s
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("x")
        }
        _ = sb.String()
    }
}

func BenchmarkMemAlloc(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := make([]byte, 1024)
        _ = s
    }
}

// Table-driven benchmark
func BenchmarkReverseString(b *testing.B) {
    cases := []struct {
        name  string
        input string
    }{
        {"short", "hello"},
        {"medium", strings.Repeat("hello", 100)},
        {"long", strings.Repeat("hello", 10000)},
    }
    for _, tc := range cases {
        b.Run(tc.name, func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                _ = reverseString(tc.input)
            }
        })
    }
}

Run benchmarks:

go test -bench=. -benchmem ./...
go test -bench=BenchmarkStringBuilder -benchmem -count=5
# Compare before/after with benchstat
benchstat old.txt new.txt

Escape Analysis

When a variable "escapes" to the heap, it requires garbage collection. Use escape analysis to understand allocation patterns:

go build -gcflags="-m" ./...
go build -gcflags="-m -m" ./..  # more verbose
// Does NOT escape: small value returned by value
func noEscape() [3]int {
    return [3]int{1, 2, 3}
}

// ESCAPES to heap: returned pointer outlives function
func escapes() *int {
    x := 42 // "x escapes to heap" reported by -gcflags=-m
    return &x
}

// Common escape trigger: interface boxing
func interfaceEscape() {
    var x int = 42
    var i interface{} = x // x may escape
    _ = i
}

sync.Pool for Reducing Allocations

sync.Pool recycles objects to reduce GC pressure:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func processRequest(data []byte) string {
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufferPool.Put(buf)

    buf.WriteString("processed: ")
    buf.Write(data)
    return buf.String()
}

Avoiding Common Allocation Hotspots

String Conversions

// BAD: unnecessary []byte -> string conversion
func badStringConversion(b []byte) bool {
    s := string(b)
    return strings.HasPrefix(s, "prefix")
}

// GOOD: use bytes package directly
func goodBytesCheck(b []byte) bool {
    return bytes.HasPrefix(b, []byte("prefix"))
}

Slice Pre-allocation

// BAD: repeated growth causes multiple allocations
func buildSliceBad(n int) []int {
    var result []int
    for i := 0; i < n; i++ {
        result = append(result, i)
    }
    return result
}

// GOOD: pre-allocate with known size
func buildSliceGood(n int) []int {
    result := make([]int, 0, n)
    for i := 0; i < n; i++ {
        result = append(result, i)
    }
    return result
}

Map Pre-sizing

// GOOD: hint the initial capacity
func buildMapGood(keys []string) map[string]int {
    m := make(map[string]int, len(keys))
    for i, k := range keys {
        m[k] = i
    }
    return m
}

Real-World Optimization Case Study

Systematic approach to optimizing a hot path:

// Original: JSON parsing in hot loop
func processEvents(raw [][]byte) []Event {
    var events []Event
    for _, data := range raw {
        var e Event
        json.Unmarshal(data, &e)
        events = append(events, e)
    }
    return events
}

// Optimized: pre-allocate slice
func processEventsOptimized(raw [][]byte) []Event {
    events := make([]Event, 0, len(raw))
    for _, data := range raw {
        var e Event
        if err := json.Unmarshal(data, &e); err != nil {
            continue
        }
        events = append(events, e)
    }
    return events
}

Using sync.Pool for encoder reuse:

var encoderPool = sync.Pool{
    New: func() interface{} {
        return &bytes.Buffer{}
    },
}

func encodeJSON(v interface{}) ([]byte, error) {
    buf := encoderPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer encoderPool.Put(buf)

    enc := json.NewEncoder(buf)
    if err := enc.Encode(v); err != nil {
        return nil, err
    }
    result := make([]byte, buf.Len())
    copy(result, buf.Bytes())
    return result, nil
}

Performance Optimization Checklist

  1. Measure first: Never optimize without profiling data.
  2. Focus on hot paths: A few functions usually account for most CPU time.
  3. Reduce allocations: Use b.ReportAllocs() in benchmarks; target zero allocs in hot paths.
  4. Pre-size collections: Hint map and slice sizes when known.
  5. Use sync.Pool: Recycle short-lived, frequently allocated objects.
  6. Avoid interface{}: Boxing causes heap allocations; use generics or concrete types.
  7. Profile in production: Development machines don't reflect production workloads.
  8. Validate improvements: Use benchstat to confirm statistically significant gains.

With Go's excellent tooling, performance optimization becomes a data-driven discipline. Profile, identify the bottleneck, make a targeted change, measure again. Repeat until you meet your SLOs.