Introduction
Choose an AWS Graviton-based instance for Go garbage collection benchmarking
Install Go and Benchstat on an AWS Graviton-based Amazon EC2 instance
Create a Go garbage collection benchmark
Run the benchmark with default Go garbage collection settings
Interpret the default garbage collection benchmark results
Experiment with garbage collection optimization
Next Steps
Create a small Go benchmark module called parsebench_test.go. The high-level flow of the benchmark is:
First, create the module directory and initialize it:
mkdir -p $HOME/go-gc-default/parsebench
cd $HOME/go-gc-default
go mod init example.com/go-gc-default
Then, create parsebench_test.go:
cat > parsebench/parsebench_test.go <<'EOF'
package parsebench
import (
"runtime"
"strconv"
"strings"
"testing"
)
// Global variable used to store benchmark results.
var sink []string
func BenchmarkParseAndAllocate(b *testing.B) {
/*
Simulates a large payload by creating a large test string by
repeating the same key=value data many times.
Example:
name=arm&runtime=go&gc=default&value=12345;
*/
payload := strings.Repeat("name=arm&runtime=go&gc=default&value=12345;",2048)
// Tells the benchmark framework to track memory allocations.
//
// This will show metrics such as allocations per operation, and bytes allocated per operation
b.ReportAllocs()
/*
Captures runtime memory statistics before the benchmark starts. You'll later compare these values to see:
- how many GCs occurred
- how much pause time was spent in GC
*/
var before runtime.MemStats
runtime.ReadMemStats(&before)
// Resets benchmark timing so that any setup work performed won't be included in the benchmark measurements.
b.ResetTimer()
/*
Benchmark loop where the actual work is done. The number of times this loop is
executed is controlled by the b.N variable. The value of b.N is automatically chosen by the Go benchmark framework to obtain stable and statistically useful measurements.
The reason for this design is that timing a single operation is often unreliable. Running it many times reduces noise from:
- OS scheduling
- CPU frequency changes
- background processes
*/
for i := 0; i < b.N; i++ {
// splits the large payload into individual records.
// Example: "a=1;b=2;c=3;" becomes: ["a=1", "b=2", "c=3", ""]
parts := strings.Split(payload, ";")
// Creates a new slice to store parsed output. This allocation is intentional for the benchmark to generate memory pressure and trigger GC activity.
out := make([]string, 0, len(parts))
// Processes each record.
for _, part := range parts {
// Ignores the empty string created by the trailing semicolon.
if part == "" {
continue
}
// Splits the string into key and value.
fields := strings.SplitN(part, "=", 2)
// Makes sure both key and value exist.
if len(fields) == 2 {
// Builds a new string containing: key:length_of_value. This creates additional allocations and string objects, increasing GC activity.
out = append(out,fields[0]+":"+strconv.Itoa(len(fields[1])),)
}
}
// Saves the result so the compiler can't eliminate the work as unused.
sink = out
}
// Stops benchmark timing.
// Everything that follows is measurement or reporting logic and shouldn't affect benchmark performance results.
b.StopTimer()
// Captures memory statistics after the benchmark completes.
var after runtime.MemStats
runtime.ReadMemStats(&after)
// Number of benchmark operations executed.
ops := float64(b.N)
// Total number of GC cycles that occurred while the benchmark was running:
gcCycles := after.NumGC - before.NumGC
/*
Total "stop-the-world" pause time spent in GC. During these pauses, application execution is temporarily halted while the runtime performs parts of GC.
*/
pauseNs := after.PauseTotalNs - before.PauseTotalNs
// Reports GC events per benchmark operation. Example: 0.002 gc/op means one GC cycle every 500 operations.
if ops > 0 {
b.ReportMetric(float64(gcCycles)/ops, "gc/op")
// Reports average GC pause time per operation.
b.ReportMetric(float64(pauseNs)/ops, "stw-ns/op")
}
// Reports the average stop-the-world pause duration for each GC cycle if at least one GC occurred.
if gcCycles > 0 {
b.ReportMetric(
float64(pauseNs)/float64(gcCycles),
"stw-ns/GC",
)
}
}
EOF
The benchmark code is now ready.
Run the following command to verify it executes without errors:
cd $HOME/go-gc-default
go test ./parsebench -run '^$' -bench BenchmarkParseAndAllocate -benchmem -count 1 -benchtime=2s
The output is similar to:
goos: linux
goarch: arm64
pkg: example.com/go-gc-default/parsebench
BenchmarkParseAndAllocate-4 14014 170814 ns/op 0.04553 gc/op 102956 stw-ns/GC 4687 stw-ns/op 163840 B/op 4098 allocs/op
PASS
ok example.com/go-gc-default/parsebench 4.127s
Your exact numbers will differ by instance type, Go version, operating system, and system load. If this test run yields results with no errors, you’re ready to move to the next section.
You’ve now created a Go GC benchmark module.
Next, you’ll run the benchmark with default GC settings.