|
| 1 | +--- |
| 2 | +title: Create a Go GC benchmark |
| 3 | +weight: 4 |
| 4 | + |
| 5 | +### FIXED, DO NOT MODIFY |
| 6 | +layout: learningpathall |
| 7 | +--- |
| 8 | + |
| 9 | +## Creating a benchmark module |
| 10 | + |
| 11 | +You'll first create a small Go benchmark module. The high-level flow is: |
| 12 | + |
| 13 | +1. Generate a large input string. |
| 14 | +2. Repeatedly parse it and create new objects/strings. |
| 15 | +3. Force memory allocations so the garbage collector has work to do. |
| 16 | +4. Measure how long the workload takes. |
| 17 | +5. Measure how much GC activity occurred during the benchmark. |
| 18 | +6. Report both performance metrics and GC-related metrics. |
| 19 | + |
| 20 | +Pasting the code below will create the module and benchmark file: |
| 21 | + |
| 22 | +```bash |
| 23 | + |
| 24 | +# Create the module directory and initialize it. |
| 25 | + |
| 26 | +mkdir -p $HOME/go-gc-default/parsebench |
| 27 | +cd $HOME/go-gc-default |
| 28 | +go mod init example.com/go-gc-default |
| 29 | + |
| 30 | +# Create the benchmark file: |
| 31 | + |
| 32 | +cat > parsebench/parsebench_test.go <<'EOF' |
| 33 | +package parsebench |
| 34 | +
|
| 35 | +import ( |
| 36 | +
|
| 37 | + "runtime" |
| 38 | + "strconv" |
| 39 | + "strings" |
| 40 | + "testing" |
| 41 | +
|
| 42 | +) |
| 43 | +
|
| 44 | +// Global variable used to store benchmark results. |
| 45 | +
|
| 46 | +var sink []string |
| 47 | +
|
| 48 | +func BenchmarkParseAndAllocate(b *testing.B) { |
| 49 | +
|
| 50 | + // This simulates a large payload by creating a large test string by |
| 51 | + // repeating the same key=value data many times. |
| 52 | + // |
| 53 | + // Example: |
| 54 | + // name=arm&runtime=go&gc=default&value=12345; |
| 55 | + // |
| 56 | + |
| 57 | + payload := strings.Repeat("name=arm&runtime=go&gc=default&value=12345;",2048) |
| 58 | + |
| 59 | + // Next, we tell the benchmark framework to track memory allocations. |
| 60 | + // |
| 61 | + // This will show metrics such as allocations per operation, and bytes allocated per operation |
| 62 | + |
| 63 | + b.ReportAllocs() |
| 64 | + |
| 65 | + // Capture runtime memory statistics before the benchmark starts. We will later compare these |
| 66 | + // values to see: |
| 67 | + // - how many garbage collections occurred |
| 68 | + // - how much pause time was spent in GC |
| 69 | + |
| 70 | + var before runtime.MemStats |
| 71 | + runtime.ReadMemStats(&before) |
| 72 | + |
| 73 | + // Reset benchmark timing so that any setup work performed above will not be included |
| 74 | + // in the benchmark measurements. |
| 75 | + |
| 76 | + b.ResetTimer() |
| 77 | + |
| 78 | + // The benchmark loop is where the actual work is done. The number of times this loop is |
| 79 | + // executed is controlled by the b.N variable. The value of b.N is automatically chosen by |
| 80 | + // the Go benchmark framework to obtain stable and statistically useful measurements. |
| 81 | + |
| 82 | + // The reason for this design is that timing a single operation is often unreliable; running |
| 83 | + // it many times reduces noise from: |
| 84 | + // * OS scheduling |
| 85 | + // * CPU frequency changes |
| 86 | + // * background processes |
| 87 | +
|
| 88 | + for i := 0; i < b.N; i++ { |
| 89 | + // split the large payload into individual records. |
| 90 | + // Example: |
| 91 | + // "a=1;b=2;c=3;" becomes: ["a=1", "b=2", "c=3", ""] |
| 92 | + parts := strings.Split(payload, ";") |
| 93 | + // Create a new slice to store parsed output. This allocation is intentional because we want |
| 94 | + // the benchmark to generate memory pressure and trigger garbage collection activity. |
| 95 | + |
| 96 | + out := make([]string, 0, len(parts)) |
| 97 | + |
| 98 | + // Process each record. |
| 99 | + |
| 100 | + for _, part := range parts { |
| 101 | + // Ignore the empty string created by the trailing semicolon. |
| 102 | + if part == "" { |
| 103 | + continue |
| 104 | + } |
| 105 | + // Split the string into key and value. |
| 106 | + |
| 107 | + fields := strings.SplitN(part, "=", 2) |
| 108 | + |
| 109 | + // Make sure both key and value exist. |
| 110 | + if len(fields) == 2 { |
| 111 | + // Build a new string containing: key:length_of_value |
| 112 | + // This creates additional allocations and string objects, increasing GC activity. |
| 113 | + out = append(out,fields[0]+":"+strconv.Itoa(len(fields[1])),) |
| 114 | + } |
| 115 | + } |
| 116 | + // Save the result so the compiler cannot eliminate the work as unused. |
| 117 | + sink = out |
| 118 | + } |
| 119 | + // Stop benchmark timing. |
| 120 | + // |
| 121 | + // Everything below is measurement/reporting logic and should not affect benchmark performance results. |
| 122 | + b.StopTimer() |
| 123 | + |
| 124 | + // Capture memory statistics after the benchmark completes. |
| 125 | + |
| 126 | + var after runtime.MemStats |
| 127 | + runtime.ReadMemStats(&after) |
| 128 | + |
| 129 | + // Number of benchmark operations executed. |
| 130 | + ops := float64(b.N) |
| 131 | + |
| 132 | + // Total number of garbage collection cycles that occurred while the benchmark was running: |
| 133 | + |
| 134 | + gcCycles := after.NumGC - before.NumGC |
| 135 | + |
| 136 | + // Total "stop-the-world" pause time spent in GC. During these pauses, application execution |
| 137 | + // is temporarily halted while the runtime performs parts of garbage collection. |
| 138 | + |
| 139 | + pauseNs := after.PauseTotalNs - before.PauseTotalNs |
| 140 | + |
| 141 | + // Report GC events per benchmark operation. Example: 0.002 gc/op means one GC cycle |
| 142 | + // every 500 operations. |
| 143 | + |
| 144 | + if ops > 0 { |
| 145 | + b.ReportMetric(float64(gcCycles)/ops, "gc/op") |
| 146 | + |
| 147 | + // Report average GC pause time per operation. |
| 148 | + b.ReportMetric(float64(pauseNs)/ops, "stw-ns/op") |
| 149 | + } |
| 150 | + // If at least one GC occurred, report the average stop-the-world pause duration for each GC cycle. |
| 151 | + if gcCycles > 0 { |
| 152 | + b.ReportMetric( |
| 153 | + float64(pauseNs)/float64(gcCycles), |
| 154 | + "stw-ns/GC", |
| 155 | + ) |
| 156 | + } |
| 157 | +
|
| 158 | +} |
| 159 | +EOF |
| 160 | +``` |
| 161 | + |
| 162 | +The benchmark code is now ready to run! Give it a try by running the following command: |
| 163 | + |
| 164 | +```bash |
| 165 | +cd $HOME/go-gc-default |
| 166 | +go test ./parsebench -run '^$' -bench BenchmarkParseAndAllocate -benchmem -count 1 -benchtime=2s |
| 167 | +``` |
| 168 | + |
| 169 | +You should see output similar to below: |
| 170 | + |
| 171 | +```output |
| 172 | +goos: linux |
| 173 | +goarch: arm64 |
| 174 | +pkg: example.com/go-gc-default/parsebench |
| 175 | +BenchmarkParseAndAllocate-4 14014 170814 ns/op 0.04553 gc/op 102956 stw-ns/GC 4687 stw-ns/op 163840 B/op 4098 allocs/op |
| 176 | +PASS |
| 177 | +ok example.com/go-gc-default/parsebench 4.127s |
| 178 | +``` |
| 179 | + |
| 180 | +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 on to the next step. |
| 181 | + |
| 182 | + |
| 183 | + |
0 commit comments