Skip to content

Commit c634147

Browse files
Merge pull request #3410 from anupras-mohapatra-arm/servers-and-cloud-computing
Benchmarking Go garbage collection editorial review
2 parents 89ea0ac + 8be4f0b commit c634147

7 files changed

Lines changed: 198 additions & 226 deletions

File tree

content/learning-paths/servers-and-cloud-computing/go-gc-default-settings/_index.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
---
2-
title: Measure Go GC behavior on AWS Graviton
3-
draft: true
4-
cascade:
5-
draft: true
2+
title: Measure and modify Go garbage collection behavior on AWS Graviton-based compute
63

7-
description: Learn how to run Go benchmarks on AWS Graviton, capture GC metrics and pprof profiles, and establish a reproducible default-GC baseline for memory-intensive workloads on Arm.
4+
description: Learn how to run Go benchmarks on AWS Graviton-based compute, capture GC metrics and pprof profiles with Benchstat, establish a reproducible default garbage collection baseline for memory-intensive workloads on Arm, and experiment with modifying garbage collection behavior.
85

96
minutes_to_complete: 75
107

118
who_is_this_for: This Learning Path is for engineers interested in learning more about Go garbage collection (GC) behavior on Arm.
129

1310
learning_objectives:
14-
- Select an AWS Graviton instance for repeatable Go GC measurements
11+
- Select an AWS Graviton-based instance for repeatable Go GC measurements
1512
- Install Go and Benchstat on an Arm Linux server
1613
- Run a Go benchmark that reports allocation, GC, and pause-time metrics
1714
- Capture CPU and heap profiles without changing GC behavior
15+
- Interpret benchmarking results and experiment with changing GC behavior
1816

1917
prerequisites:
20-
- An [AWS account](https://aws.amazon.com/) with permission to launch AWS Graviton EC2 instances
18+
- An [AWS account](https://aws.amazon.com/) with permission to launch an AWS Graviton-based Amazon EC2 instance running Ubuntu 24.04 LTS or another Arm Linux distribution
2119
- The [AWS CLI](/install-guides/aws-cli/) installed and configured on your local machine
22-
- An AWS Graviton instance running Ubuntu 24.04 LTS or another Arm Linux distribution
2320
- Basic familiarity with Go benchmarks and Linux shell commands
2421

2522
author: Geremy Cohen
@@ -37,6 +34,7 @@ armips:
3734
- Neoverse
3835
tools_software_languages:
3936
- Go
37+
- Benchstat
4038
operatingsystems:
4139
- Linux
4240

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
11
---
2-
title: Choose an AWS Graviton instance
2+
title: Choose an AWS Graviton-based instance for Go garbage collection benchmarking
3+
description: Choose and launch an AWS Graviton-based Amazon EC2 instance for repeatable Go garbage collection benchmark measurements.
34
weight: 2
45

56
### FIXED, DO NOT MODIFY
67
layout: learningpathall
78
---
8-
## What is Garbage Collection? (GC)
9-
Memory management is a critical aspect of application performance, and Garbage Collection (GC) plays a central role in automating that process. GC continuously identifies and removes objects that are no longer needed, freeing memory for reuse.
9+
## Understand Go garbage collection
1010

11-
While this automation improves productivity and application safety, inefficient garbage collection can lead to increased CPU usage, longer response times, and unexpected application pauses.
11+
Memory management is a critical aspect of application performance. Garbage collection (GC) plays a important role in automating memory management. GC continuously identifies and removes objects that are no longer needed, freeing memory for reuse.
1212

13-
Tracking GC metrics provides a window into an application's memory health, helping engineers optimize performance, and ensuring the system can scale efficiently under load.
13+
This automation improves productivity and application safety. However, inefficient GC can lead to increased CPU usage, longer response times, and unexpected application pauses.
1414

15-
## Measuring default Go GC behavior on Arm servers
15+
Tracking GC metrics provides a window into an application's memory health that you can use to optimize performance and ensure the system can scale efficiently under load.
1616

17-
Go is one such language that implements GC. As Go applications can spend meaningful time allocating memory and running garbage collection, it is important to understand how the Go runtime behaves under default settings.
17+
Go applications can spend meaningful time allocating memory and running GC. This makes it important to understand how the Go runtime behaves under default settings.
1818

19-
In this Learning Path, you'll run Go benchmarks on an AWS Graviton instance. The goal is to build a clean baseline, measuring operation time, allocation rate, GC frequency, and GC pause cost.
19+
In this Learning Path, you'll run Go benchmarks on an Amazon EC2 instance powered by AWS Graviton. The goal is to build a clean baseline measuring operation time, allocation rate, GC frequency, and GC pause cost.
2020

21-
## Selecting an instance for Go GC measurements
21+
## Select an instance for Go garbage collection measurements
2222

23-
An AWS Graviton `m8g.xlarge` instance has enough CPU and memory to make Go runtime behavior visible, while keeping costs minimal. It's a good starting point as it provides four vCPUs and 16 GiB of memory on AWS Graviton4. If you choose to run this Learning Path on a different instance, make sure it has at least 4 vCPUs and 16 GiB of memory to ensure the benchmark runs smoothly and provides meaningful GC metrics.
24-
25-
Avoid burstable `t4g` instances as CPU credits can affect benchmark repeatability and make GC measurements harder to explain.
23+
An `m8g.xlarge` instance powered by AWS Graviton has enough CPU and memory to make Go runtime behavior visible, while keeping costs minimal. It's a good starting point because it provides four vCPUs and 16 GiB of memory on AWS Graviton4.
2624

2725
{{% notice Note %}}
28-
You can use larger instances, such as `m8g.2xlarge`, when you want more CPU width or more memory headroom. Start with `m8g.xlarge` so the first benchmark run is easy to reproduce and inexpensive.
29-
{{% /notice %}}
26+
You can use larger instances, such as `m8g.2xlarge`, when you want more CPU capacity or memory. Start with `m8g.xlarge` so the first benchmark run is easy to reproduce and inexpensive.
3027

28+
If you choose to run this Learning Path on a different instance, make sure it has at least four vCPUs and 16 GiB of memory to ensure the benchmark runs smoothly and provides meaningful GC metrics.
3129

32-
## Checking instance availability
30+
Avoid burstable `t4g` instances as CPU credits can affect benchmark repeatability and make GC measurements harder to explain.
31+
{{% /notice %}}
3332

34-
Use the AWS CLI to check whether `m8g.xlarge` is available in your selected Region.
3533

36-
Replace `us-east-1` with the Region you want to use.
34+
### Check instance availability
35+
36+
Use the AWS CLI to check whether `m8g.xlarge` is available in your selected AWS Region, replacing `us-east-1` with the Region you want to use:
3737

3838
```console
3939
aws ec2 describe-instance-type-offerings \
@@ -44,7 +44,9 @@ aws ec2 describe-instance-type-offerings \
4444
--output table
4545
```
4646

47-
If the command returns one or more Availability Zones, you can use `m8g.xlarge` in that Region. If `m8g.xlarge` is not available in your Region, try a different Region, or fall back to an `m7g.xlarge` instance, which is based on the previous generation AWS Graviton3:
47+
If the command returns one or more Availability Zones, you can use `m8g.xlarge` in that Region.
48+
49+
If `m8g.xlarge` is not available in your Region, try a different Region, or fall back to an `m7g.xlarge` instance, which is based on the previous generation AWS Graviton3:
4850

4951
```console
5052
aws ec2 describe-instance-type-offerings \
@@ -55,7 +57,9 @@ aws ec2 describe-instance-type-offerings \
5557
--output table
5658
```
5759

58-
After selecting an instance type, launch it with the Ubuntu 24.04 LTS (arm64) AMI. You can do this through the [AWS EC2 console](https://console.aws.amazon.com/ec2/) or the AWS CLI. When configuring the instance:
60+
### Launch an instance
61+
62+
After selecting an instance type, launch it with the Ubuntu 24.04 LTS (arm64) AMI. You can do this using the [Amazon EC2 console](https://console.aws.amazon.com/ec2/) or the AWS CLI. When configuring the instance:
5963

6064
- Select your chosen instance type (`m8g.xlarge` or `m7g.xlarge`)
6165
- Choose the Ubuntu 24.04 LTS arm64 AMI for your Region
@@ -67,5 +71,8 @@ After the instance reaches the running state, connect to it over SSH:
6771
```bash
6872
ssh -i /path/to/your-key.pem ubuntu@<instance-public-ip>
6973
```
74+
## What you've accomplished and what's next
75+
76+
You now know why measuring Go GC behavior is useful, and you've deployed an Arm-based Amazon EC2 instance powered by AWS Graviton.
7077

71-
You now have a running Arm Linux instance on AWS Graviton. In the next section you'll install Go and Benchstat so the instance is ready to run benchmarks.
78+
Next, you'll install Go and Benchstat on the instance so that you can run GC benchmarks.
Lines changed: 59 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
---
2-
title: Create a Go GC benchmark
2+
title: Create a Go garbage collection benchmark
3+
description: Create and verify a Go benchmark that generates allocation pressure and reports garbage collection metrics on Arm Linux.
34
weight: 4
45

56
### FIXED, DO NOT MODIFY
67
layout: learningpathall
78
---
89

9-
## Creating a benchmark module
10+
## Create a benchmark module
1011

11-
You'll first create a small Go benchmark module. The high-level flow is:
12+
Create a small Go benchmark module called `parsebench_test.go`. The high-level flow of the benchmark is:
1213

1314
1. Generate a large input string.
14-
2. Repeatedly parse it and create new objects/strings.
15+
2. Repeatedly parse it and create new objects and strings.
1516
3. Force memory allocations so the garbage collector has work to do.
1617
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.
18+
5. Measure how much garbage collection (GC) activity occurred during the benchmark.
19+
6. Report both performance metrics and GC metrics.
1920

20-
Create the module directory and initialize it:
21+
First, create the module directory and initialize it:
2122

2223
```bash
2324
mkdir -p $HOME/go-gc-default/parsebench
2425
cd $HOME/go-gc-default
2526
go mod init example.com/go-gc-default
2627
```
2728

28-
Create the benchmark file:
29+
Then, create `parsebench_test.go`:
2930

3031
```bash
3132
cat > parsebench/parsebench_test.go <<'EOF'
@@ -46,107 +47,104 @@ var sink []string
4647
4748
func BenchmarkParseAndAllocate(b *testing.B) {
4849
49-
// This simulates a large payload by creating a large test string by
50-
// repeating the same key=value data many times.
51-
//
52-
// Example:
53-
// name=arm&runtime=go&gc=default&value=12345;
54-
//
50+
/*
51+
Simulates a large payload by creating a large test string by
52+
repeating the same key=value data many times.
53+
54+
Example:
55+
name=arm&runtime=go&gc=default&value=12345;
56+
*/
5557
5658
payload := strings.Repeat("name=arm&runtime=go&gc=default&value=12345;",2048)
5759
58-
// Next, we tell the benchmark framework to track memory allocations.
60+
// Tells the benchmark framework to track memory allocations.
5961
//
6062
// This will show metrics such as allocations per operation, and bytes allocated per operation
6163
6264
b.ReportAllocs()
6365
64-
// Capture runtime memory statistics before the benchmark starts. We will later compare these
65-
// values to see:
66-
// - how many garbage collections occurred
67-
// - how much pause time was spent in GC
66+
/*
67+
Captures runtime memory statistics before the benchmark starts. You'll later compare these values to see:
68+
- how many GCs occurred
69+
- how much pause time was spent in GC
70+
*/
6871
6972
var before runtime.MemStats
7073
runtime.ReadMemStats(&before)
7174
72-
// Reset benchmark timing so that any setup work performed above will not be included
73-
// in the benchmark measurements.
75+
// Resets benchmark timing so that any setup work performed won't be included in the benchmark measurements.
7476
7577
b.ResetTimer()
7678
77-
// The benchmark loop is where the actual work is done. The number of times this loop is
78-
// executed is controlled by the b.N variable. The value of b.N is automatically chosen by
79-
// the Go benchmark framework to obtain stable and statistically useful measurements.
80-
81-
// The reason for this design is that timing a single operation is often unreliable; running
82-
// it many times reduces noise from:
83-
// * OS scheduling
84-
// * CPU frequency changes
85-
// * background processes
79+
/*
80+
Benchmark loop where the actual work is done. The number of times this loop is
81+
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.
82+
83+
The reason for this design is that timing a single operation is often unreliable. Running it many times reduces noise from:
84+
- OS scheduling
85+
- CPU frequency changes
86+
- background processes
87+
*/
8688
8789
for i := 0; i < b.N; i++ {
88-
// split the large payload into individual records.
89-
// Example:
90-
// "a=1;b=2;c=3;" becomes: ["a=1", "b=2", "c=3", ""]
90+
// splits the large payload into individual records.
91+
// Example: "a=1;b=2;c=3;" becomes: ["a=1", "b=2", "c=3", ""]
9192
parts := strings.Split(payload, ";")
92-
// Create a new slice to store parsed output. This allocation is intentional because we want
93-
// the benchmark to generate memory pressure and trigger garbage collection activity.
93+
// Creates a new slice to store parsed output. This allocation is intentional for the benchmark to generate memory pressure and trigger GC activity.
9494
9595
out := make([]string, 0, len(parts))
9696
97-
// Process each record.
97+
// Processes each record.
9898
9999
for _, part := range parts {
100-
// Ignore the empty string created by the trailing semicolon.
100+
// Ignores the empty string created by the trailing semicolon.
101101
if part == "" {
102102
continue
103103
}
104-
// Split the string into key and value.
104+
// Splits the string into key and value.
105105
106106
fields := strings.SplitN(part, "=", 2)
107107
108-
// Make sure both key and value exist.
108+
// Makes sure both key and value exist.
109109
if len(fields) == 2 {
110-
// Build a new string containing: key:length_of_value
111-
// This creates additional allocations and string objects, increasing GC activity.
110+
// Builds a new string containing: key:length_of_value. This creates additional allocations and string objects, increasing GC activity.
112111
out = append(out,fields[0]+":"+strconv.Itoa(len(fields[1])),)
113112
}
114113
}
115-
// Save the result so the compiler cannot eliminate the work as unused.
114+
// Saves the result so the compiler can't eliminate the work as unused.
116115
sink = out
117116
}
118-
// Stop benchmark timing.
119-
//
120-
// Everything below is measurement/reporting logic and should not affect benchmark performance results.
117+
// Stops benchmark timing.
118+
// Everything that follows is measurement or reporting logic and shouldn't affect benchmark performance results.
121119
b.StopTimer()
122120
123-
// Capture memory statistics after the benchmark completes.
121+
// Captures memory statistics after the benchmark completes.
124122
125123
var after runtime.MemStats
126124
runtime.ReadMemStats(&after)
127125
128126
// Number of benchmark operations executed.
129127
ops := float64(b.N)
130128
131-
// Total number of garbage collection cycles that occurred while the benchmark was running:
129+
// Total number of GC cycles that occurred while the benchmark was running:
132130
133131
gcCycles := after.NumGC - before.NumGC
134132
135-
// Total "stop-the-world" pause time spent in GC. During these pauses, application execution
136-
// is temporarily halted while the runtime performs parts of garbage collection.
133+
/*
134+
Total "stop-the-world" pause time spent in GC. During these pauses, application execution is temporarily halted while the runtime performs parts of GC.
135+
*/
137136
138137
pauseNs := after.PauseTotalNs - before.PauseTotalNs
139138
140-
// Report GC events per benchmark operation. Example: 0.002 gc/op means one GC cycle
141-
// every 500 operations.
139+
// Reports GC events per benchmark operation. Example: 0.002 gc/op means one GC cycle every 500 operations.
142140
143141
if ops > 0 {
144142
b.ReportMetric(float64(gcCycles)/ops, "gc/op")
145143
146-
// Report average GC pause time per operation.
144+
// Reports average GC pause time per operation.
147145
b.ReportMetric(float64(pauseNs)/ops, "stw-ns/op")
148146
}
149-
// If at least one GC occurred, report the average stop-the-world pause duration for each GC cycle.
147+
// Reports the average stop-the-world pause duration for each GC cycle if at least one GC occurred.
150148
if gcCycles > 0 {
151149
b.ReportMetric(
152150
float64(pauseNs)/float64(gcCycles),
@@ -158,14 +156,16 @@ func BenchmarkParseAndAllocate(b *testing.B) {
158156
EOF
159157
```
160158

161-
The benchmark code is now ready. Run the following command to verify it executes without errors:
159+
The benchmark code is now ready.
160+
161+
Run the following command to verify it executes without errors:
162162

163163
```bash
164164
cd $HOME/go-gc-default
165165
go test ./parsebench -run '^$' -bench BenchmarkParseAndAllocate -benchmem -count 1 -benchtime=2s
166166
```
167167

168-
You should see output similar to below:
168+
The output is similar to:
169169

170170
```output
171171
goos: linux
@@ -176,7 +176,10 @@ PASS
176176
ok example.com/go-gc-default/parsebench 4.127s
177177
```
178178

179-
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.
179+
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.
180180

181+
## What you've accomplished and what's next
181182

183+
You've now created a Go GC benchmark module.
182184

185+
Next, you'll run the benchmark with default GC settings.

0 commit comments

Comments
 (0)