Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Measure Go GC behavior on AWS Graviton
description: Learn how to measure and observe Go garbage collection metrics on AWS Graviton instances.

minutes_to_complete: 75

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

learning_objectives:
- Select an AWS Graviton instance for repeatable Go GC measurements
- Install Go and Benchstat on an Arm Linux server
- Run a Go benchmark that reports allocation, GC, and pause-time metrics
- Capture CPU and heap profiles without changing GC behavior

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

author: Geremy Cohen

### Tags
skilllevels: Introductory
subjects: Performance and Architecture
cloud_service_providers:
- AWS
armips:
- Neoverse
tools_software_languages:
- AWS
- Go
operatingsystems:
- Linux

further_reading:
- resource:
title: Amazon EC2 M8g instances
link: https://aws.amazon.com/ec2/instance-types/m8g/
type: documentation
- resource:
title: Go GC guide
link: https://go.dev/doc/gc-guide
type: documentation
- resource:
title: Go runtime package
link: https://pkg.go.dev/runtime
type: documentation
- resource:
title: Go testing package
link: https://pkg.go.dev/testing
type: documentation
- resource:
title: Graviton Performance Runbook
link: https://github.com/aws/aws-graviton-getting-started/blob/main/perfrunbook/README.md
type: documentation
- resource:
title: Benchmark Go performance with Sweet and Benchstat
link: /learning-paths/servers-and-cloud-computing/go-benchmarking-with-sweet/
type: learning path

### FIXED, DO NOT MODIFY
# ================================================================================
weight: 1 # _index.md always has weight of 1 to order correctly
layout: "learningpathall" # All files under learning paths have this same wrapper
learning_path_main_page: "yes" # This should be surfaced when looking for related content. Only set for _index.md of learning path content.
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
# ================================================================================
# FIXED, DO NOT MODIFY THIS FILE
# ================================================================================
weight: 21 # The weight controls the order of the pages. _index.md always has weight 1.
title: "Next Steps" # Always the same, html page title.
layout: "learningpathall" # All files under learning paths have this same wrapper for Hugo processing.
---

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: Choose an AWS Graviton instance
weight: 2

### FIXED, DO NOT MODIFY
layout: learningpathall
---
## What is Garbage Collection? (GC)
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..

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

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.

## Measuring default Go GC behavior on Arm servers

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.

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.

## Selecting an instance for Go GC measurements

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.

Avoid burstable `t4g` instances as CPU credits can affect benchmark repeatability and make GC measurements harder to explain.

{{% notice Note %}}
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.
{{% /notice %}}


## Checking instance availability

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

Replace `us-east-1` with the Region you want to use.

```console
aws ec2 describe-instance-type-offerings \
--region us-east-1 \
--location-type availability-zone \
--filters Name=instance-type,Values=m8g.xlarge \
--query 'InstanceTypeOfferings[].Location' \
--output table
```

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:

```console
aws ec2 describe-instance-type-offerings \
--region us-east-1 \
--location-type availability-zone \
--filters Name=instance-type,Values=m7g.xlarge \
--query 'InstanceTypeOfferings[].Location' \
--output table
```

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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
---
title: Create a Go GC benchmark
weight: 4

### FIXED, DO NOT MODIFY
layout: learningpathall
---

## Creating a benchmark module

You'll first create a small Go benchmark module. The high-level flow is:

1. Generate a large input string.
2. Repeatedly parse it and create new objects/strings.
3. Force memory allocations so the garbage collector has work to do.
4. Measure how long the workload takes.
5. Measure how much GC activity occurred during the benchmark.
6. Report both performance metrics and GC-related metrics.

Pasting the code below will create the module and benchmark file:

```bash

# 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

# Create the benchmark file:

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) {

// This 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)

// Next, we tell the benchmark framework to track memory allocations.
//
// This will show metrics such as allocations per operation, and bytes allocated per operation

b.ReportAllocs()

// Capture runtime memory statistics before the benchmark starts. We will later compare these
// values to see:
// - how many garbage collections occurred
// - how much pause time was spent in GC

var before runtime.MemStats
runtime.ReadMemStats(&before)

// Reset benchmark timing so that any setup work performed above will not be included
// in the benchmark measurements.

b.ResetTimer()

// The benchmark loop is 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++ {
// split the large payload into individual records.
// Example:
// "a=1;b=2;c=3;" becomes: ["a=1", "b=2", "c=3", ""]
parts := strings.Split(payload, ";")
// Create a new slice to store parsed output. This allocation is intentional because we want
// the benchmark to generate memory pressure and trigger garbage collection activity.

out := make([]string, 0, len(parts))

// Process each record.

for _, part := range parts {
// Ignore the empty string created by the trailing semicolon.
if part == "" {
continue
}
// Split the string into key and value.

fields := strings.SplitN(part, "=", 2)

// Make sure both key and value exist.
if len(fields) == 2 {
// Build 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])),)
}
}
// Save the result so the compiler cannot eliminate the work as unused.
sink = out
}
// Stop benchmark timing.
//
// Everything below is measurement/reporting logic and should not affect benchmark performance results.
b.StopTimer()

// Capture 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 garbage collection 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 garbage collection.

pauseNs := after.PauseTotalNs - before.PauseTotalNs

// Report 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")

// Report average GC pause time per operation.
b.ReportMetric(float64(pauseNs)/ops, "stw-ns/op")
}
// If at least one GC occurred, report the average stop-the-world pause duration for each GC cycle.
if gcCycles > 0 {
b.ReportMetric(
float64(pauseNs)/float64(gcCycles),
"stw-ns/GC",
)
}

}
EOF
```

The benchmark code is now ready to run! Give it a try by running the following command:

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

You should see output similar to below:

```output
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 on to the next step.



Loading
Loading