Skip to content

Commit 4a0ba80

Browse files
committed
Go GC Tech Review
1 parent a1e0aa1 commit 4a0ba80

7 files changed

Lines changed: 146 additions & 167 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ draft: true
44
cascade:
55
draft: true
66

7-
description: Learn how to measure and observe Go garbage collection metrics on AWS Graviton instances.
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.
88

99
minutes_to_complete: 75
1010

@@ -32,7 +32,6 @@ cloud_service_providers:
3232
armips:
3333
- Neoverse
3434
tools_software_languages:
35-
- AWS
3635
- Go
3736
operatingsystems:
3837
- Linux

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ weight: 2
66
layout: learningpathall
77
---
88
## What is Garbage Collection? (GC)
9-
Memory management is a critical aspects 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 re-use for other purposes..
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.
1010

1111
While this automation improves productivity and application safety, inefficient garbage collection can lead to increased CPU usage, longer response times, and unexpected application pauses.
1212

1313
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.
1414

1515
## Measuring default Go GC behavior on Arm servers
1616

17-
Go is one such language which 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 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.
1818

1919
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.
2020

@@ -44,7 +44,7 @@ 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 you are unable to find `m8g.xlarge` in your Region, you can try a different Region, or fallback 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. 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:
4848

4949
```console
5050
aws ec2 describe-instance-type-offerings \
@@ -55,4 +55,17 @@ aws ec2 describe-instance-type-offerings \
5555
--output table
5656
```
5757

58-
Once you have chosen an instance type, provision it to run Ubuntu 24.04 LTS Arm64. Once the instance is running, and you are ssh'd into it, you can proceed to the next step.
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:
59+
60+
- Select your chosen instance type (`m8g.xlarge` or `m7g.xlarge`)
61+
- Choose the Ubuntu 24.04 LTS arm64 AMI for your Region
62+
- Configure a security group that allows inbound SSH (port 22) from your IP address
63+
- Associate an existing key pair or create a new one for SSH access
64+
65+
After the instance reaches the running state, connect to it over SSH:
66+
67+
```bash
68+
ssh -i /path/to/your-key.pem ubuntu@<instance-public-ip>
69+
```
70+
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.

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@ You'll first create a small Go benchmark module. The high-level flow is:
1717
5. Measure how much GC activity occurred during the benchmark.
1818
6. Report both performance metrics and GC-related metrics.
1919

20-
Pasting the code below will create the module and benchmark file:
20+
Create the module directory and initialize it:
2121

2222
```bash
23-
24-
# Create the module directory and initialize it.
25-
2623
mkdir -p $HOME/go-gc-default/parsebench
2724
cd $HOME/go-gc-default
2825
go mod init example.com/go-gc-default
26+
```
2927

30-
# Create the benchmark file:
28+
Create the benchmark file:
3129

30+
```bash
3231
cat > parsebench/parsebench_test.go <<'EOF'
3332
package parsebench
3433
@@ -159,7 +158,7 @@ func BenchmarkParseAndAllocate(b *testing.B) {
159158
EOF
160159
```
161160

162-
The benchmark code is now ready to run! Give it a try by running the following command:
161+
The benchmark code is now ready. Run the following command to verify it executes without errors:
163162

164163
```bash
165164
cd $HOME/go-gc-default

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,36 @@ The following commands use Go 1.26.3. The same commands work with other Go versi
1515
{{% /notice %}}
1616

1717

18-
```bash
19-
# Download the Go archive and verify the checksum:
18+
Download the Go archive and verify the checksum:
2019

20+
```bash
2121
cd $HOME
2222
curl -LO https://go.dev/dl/go1.26.3.linux-arm64.tar.gz
2323
echo "9d89a3ea57d141c2b22d70083f2c8459ba3890f2d9e818e7e933b75614936565 go1.26.3.linux-arm64.tar.gz" | sha256sum -c -
24+
```
2425

25-
# Install Go under `/usr/local`:
26+
Install Go under `/usr/local`:
2627

28+
```bash
2729
sudo rm -rf /usr/local/go
2830
sudo tar -C /usr/local -xzf go1.26.3.linux-arm64.tar.gz
31+
```
32+
33+
Add Go to your shell path:
2934

30-
# Add Go to your shell path:
35+
```bash
3136
export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH
37+
```
3238

33-
# To make the path update persistent, add it to your shell profile:
39+
To make the path update persistent, add it to your shell profile:
3440

41+
```bash
3542
echo 'export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH' >> $HOME/.profile
43+
```
44+
45+
Verify that Go is installed for Arm64 Linux:
3646

37-
# Verify that Go is installed for Arm64 Linux:
38-
echo
47+
```bash
3948
go version
4049
go env GOOS GOARCH
4150
```
@@ -50,7 +59,7 @@ arm64
5059

5160
## Installing Benchstat
5261

53-
`benchstat` is a Go performance analysis tool that compares benchmark results and provides statistical analysis of performance differences between runs. It helps developers determine whether observed changes in benchmark metrics are statistically significant rather than simply the result of normal measurement variability. We'll use `benchstat` for that purpose in this learning path.
62+
`benchstat` is a Go performance analysis tool that compares benchmark results and provides statistical analysis of performance differences between runs. It helps developers determine whether observed changes in benchmark metrics are statistically significant rather than simply the result of normal measurement variability. You'll use `benchstat` for that purpose in this Learning Path.
5463

5564
To install `benchstat`:
5665

@@ -66,7 +75,8 @@ go: downloading github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38
6675
```
6776

6877
Finally, do a quick check to make sure `benchstat` is installed:
69-
```console
78+
79+
```bash
7080
benchstat -h
7181
```
7282

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

Lines changed: 10 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ layout: learningpathall
88

99
## Understanding the benchmark metrics
1010

11-
To understand better what these benchmarks results are showing, firt open the Benchstat summary:
11+
To understand what the benchmark results are showing, first open the Benchstat summary:
1212

1313
```bash
1414
cat default_gc_benchstat.txt
1515
```
1616

17-
The the metrics you see explain the following:
17+
The metrics you see explain the following:
1818

1919
| Metric | Read |
2020
| --- | --- |
@@ -37,6 +37,8 @@ cat cpu_default_top.txt
3737

3838
Look for functions that dominate CPU time. In an allocation-heavy benchmark, you can expect to see time in string handling, allocation paths, and some runtime or GC support functions.
3939

40+
The `flat` column shows CPU time spent directly in that function. The `cum` (cumulative) column includes time spent in the function and all functions it called. A function with low `flat` but high `cum` is spending most of its time in callees, which can point to allocation chains or deep call stacks.
41+
4042
On the validated `m8g.xlarge` instance, the top CPU profile entries included string scanning, string concatenation, split handling, and allocation paths:
4143

4244
```output
@@ -48,11 +50,9 @@ On the validated `m8g.xlarge` instance, the top CPU profile entries included str
4850
0.72s 4.56% 53.36% 5.17s 32.76% runtime.mallocgc
4951
```
5052

51-
Open the heap allocation profile summary:
53+
`IndexByteString` and `concatstrings` dominate because the benchmark splits the payload with `strings.Split` (which scans for `;` byte-by-byte) and builds `key:length` strings with `+` concatenation on every iteration. These operations create a large volume of short-lived strings, giving the GC constant work to do. On Graviton, `pprof` makes these allocation chains directly visible because the Arm64 stack unwinder captures the full call path without frame pointer ambiguity.
5254

53-
```bash
54-
cat mem_default_alloc_top.txt
55-
```
55+
Open the heap allocation profile summary:
5656

5757
Look for application functions that allocate the most heap memory. Reducing allocation volume in those functions usually gives the Go GC less work to do.
5858

@@ -68,7 +68,7 @@ On the validated `m8g.xlarge` instance, the allocation profile showed that `stri
6868

6969
## Keeping a baseline to compare to future changes
7070

71-
These results show your default Go GC baseline stats for this benchmarking app on AWS Graviton. By keeping the following files together (eg, each stored in their own zip file, folder, etc) you can easily see how making code changes affects your apps overall performance:
71+
These results show your default Go GC baseline for this benchmarking app on AWS Graviton. By keeping the following files together — for example, in a versioned folder or archive — you can see how code changes affect overall performance:
7272

7373
```output
7474
default_runtime_baseline.txt
@@ -81,133 +81,8 @@ mem_default.out
8181
mem_default_alloc_top.txt
8282
```
8383

84-
## Experiment with Code Changes that influence GC Behavior
85-
Now that you have a baseline, you can experiment with code changes that influence GC behavior. For example, you could try:
86-
87-
## Challenge 1
88-
89-
You just found out that the payload size this benchmark is intended to represent is actually only 128 records instead of 2048. What changes can we make from the baseline to test whether optimizing for this smaller workload affects GC frequency, pause times, and overall application performance?
90-
91-
### Idea: Reduce the payload size
92-
93-
### How
94-
95-
```go
96-
payload := strings.Repeat(
97-
"name=arm&runtime=go&gc=default&value=12345;",
98-
512,
99-
)
100-
```
101-
102-
### Why
103-
104-
A smaller payload creates fewer temporary objects and less garbage each iteration.
105-
106-
---
107-
108-
## Challenge 2
109-
110-
After profiling the application, you discover that the input payload rarely changes between requests. What modifications can we make to reuse preprocessing work and determine whether reducing repeated allocations improves GC behavior and throughput?
111-
112-
### Idea: Move `strings.Split(payload, ";")` outside the benchmark loop
113-
114-
### How
115-
116-
```go
117-
parts := strings.Split(payload, ";")
118-
119-
b.ResetTimer()
120-
121-
for i := 0; i < b.N; i++ {
122-
...
123-
}
124-
```
125-
126-
### Why
127-
128-
This avoids repeatedly allocating the same slice of records on every iteration.
129-
130-
---
131-
132-
## Challenge 3
133-
134-
The benchmark currently creates a new output buffer for every operation, but production code processes millions of requests using the same worker. How can we modify the benchmark to reuse memory and evaluate the impact on GC activity and memory consumption?
135-
136-
### Idea: Reuse the output slice
137-
138-
### How
139-
140-
```go
141-
out := make([]string, 0, len(parts))
142-
143-
for i := 0; i < b.N; i++ {
144-
out = out[:0]
145-
...
146-
}
147-
```
148-
149-
### Why
150-
151-
Reusing the backing array reduces allocations and GC pressure.
152-
153-
---
154-
155-
## Challenge 4
156-
157-
A CPU profile shows that string parsing is one of the hottest code paths in the application. What changes can we make to reduce temporary allocations during parsing and measure whether this reduces GC overhead?
158-
159-
### Idea: Replace `strings.SplitN()` with `strings.IndexByte()`
160-
161-
### How
162-
163-
```go
164-
idx := strings.IndexByte(part, '=')
165-
if idx >= 0 {
166-
key := part[:idx]
167-
value := part[idx+1:]
168-
...
169-
}
170-
```
171-
172-
### Why
173-
174-
This avoids allocating a temporary `[]string` for every record processed.
175-
176-
---
177-
178-
## Challenge 5
179-
180-
Product requirements change and the application no longer needs to generate derived `"key:length"` strings. What modifications can we make to avoid unnecessary string allocations and test their effect on garbage collection performance?
181-
182-
### Idea: Avoid creating new strings in the hot loop
183-
184-
### How
185-
186-
```go
187-
out = append(out, fields[0])
188-
```
189-
190-
### Why
191-
192-
Instead of building `"key:length"` strings, store existing strings or simpler values to reduce allocations.
193-
194-
---
195-
196-
## Challenge 6
197-
198-
Usage analytics show that most customers send payloads that are half the size represented by the current benchmark. How can we adjust the workload to better reflect real-world traffic and evaluate whether the resulting reduction in allocations improves GC efficiency?
199-
200-
### Idea: Reduce the number of records processed
201-
202-
### How
203-
204-
```go
205-
payload := strings.Repeat(
206-
"name=arm&runtime=go&gc=default&value=12345;",
207-
1024,
208-
)
209-
```
84+
## What you've accomplished and what's next
21085

211-
### Why
86+
In this section, you ran Benchstat to summarize ten benchmark samples and inspected CPU and heap profiles to identify the hottest code paths. You now have a documented default-GC baseline showing operation time, allocation rate, GC frequency, and stop-the-world pause costs on AWS Graviton.
21287

213-
Fewer records means less allocation work and fewer GC cycles.
88+
In the next section, you'll apply code changes to the benchmark and measure how each change affects these metrics.

0 commit comments

Comments
 (0)