Boost performance of Go applications with profile-guided optimization
Cameron Balahan
Group Product Manager, Go Programming Language
James Ma
Sr Product Manager
November 15, 2024: The Cloud Innovators Plus program has evolved and is now the premium tier of the Google Developer Program.
In 2023, Go 1.21 introduced profile-guided optimization, or PGO. With PGO, you can provide the Go compiler a profile of your application at runtime, which the Go compiler then uses to make smarter decisions about how to optimize your code on the next build.
Teams at Google and Uber collaborated to build PGO beginning in 2021. Together, we experimented with various optimizations to increase compute efficiency and reduce costs. Today, Uber has rolled out PGO fleet-wide, with a reduction in CPU utilization across many of their services. Read more to learn how you, too, can use PGO in your Go applications, including on Google Cloud.
What is PGO?
When you build a Go binary, the Go compiler performs optimizations to try to generate the best performing binary it can. But this is not always an easy task: In many cases there are trade-offs where overly aggressive optimization can actually hurt performance or cause excessive build times. Without knowing how your code is used at runtime, the compiler uses static heuristics to make best guesses at what the most commonly invoked paths of the code are and then optimizes accordingly.
But what if you could tell the compiler exactly how your code is used at runtime? With PGO, you can. If you collect a profile of your application in production and then use this profile on the next build, the compiler can make better-informed decisions like more aggressively optimizing the most frequently used functions, or more accurately selecting common cases inside a function.
Using PGO in your Go application
Using PGO is straightforward. Simply collect a profile of your application at runtime, and then provide the compiler the profile on your next build. Here’s how to do that:
1. Import and enable profiling: In your main
package, import the net/http/pprof
package. This automatically adds a /debug/pprof/profile
endpoint to the server for fetching a CPU profile.
2. Collect a profile: Build your project as usual, and then run your application in a representative environment like production, staging, or under realistic test conditions. While your application is running and experiencing typical load, download a profile from the server endpoint you created in the last step. For example, if your application runs locally:
3. Use the profile to optimize your next build: Now that you have a profile, you can use it in your next build. The Go toolchain automatically enables PGO when it finds a profile named default.pgo
in the main package directory. Alternatively, the -pgo
flag to go build
takes a path to a profile to use for PGO. We recommend committing default.pgo
files to your repository so that users automatically have access to the profile and your builds remain reproducible (and performant!):
4. Measure improvements: If you are able to replicate the conditions in which you created your first profile (e.g., with a load test that provides a constant number of queries per second), then you can collect a new profile with your optimized build and compare it against the first one using the go tool pprof
command to measure CPU usage reductions:
For a detailed example with more information on how to benchmark your performance improvements, be sure to check out this post from the Go blog. You can also learn how PGO works under the hood as well as how to generate more robust profiling strategies on the Go docs.
Using PGO on Google Cloud with Cloud Run and Cloud Profiler
Using PGO on Google Cloud is even easier. With Cloud Run, you get access to a container platform that’s simple, automated, and hyper-elastic. You can deploy your Go service directly onto Cloud Run with a single command and, because it is serverless, you only pay for exactly what you use. Once you have your Go service deployed, you can enable Cloud Profiler and collect a profile against production-like traffic. Once you have enough data, you can download your profile and update your Go service on Cloud Run. Here’s how to do that:
1. Deploy your Go app to Cloud Run: Start by deploying your application to Cloud Run. If you like, use the the --source
flag to gcloud run deploy
from your project’s root directory to automatically compile and deploy at once:
When the deployment completes, you will get a unique *.run.app
URL for your running Go service.
2. Collect a profile: Run your application under realistic test conditions. While your application is running and experiencing typical load, collect a profile from Cloud Profiler and download it:
3. Use the profile to optimize your next build: As before, enable PGO by renaming the profile you downloaded in the previous step to default.pgo
and moving it to your project’s main package directory:
4. Measure improvements: Using Cloud Run’s metrics dashboard, monitor improvements in billable container instance time and container CPU utilization:
Learn more
If you’re just getting started with Google Cloud or want to learn more on how you can use Google Cloud’s solutions for your applications, consider this quickstart on how to deploy a Go service on Cloud Run. If you’re ready to dive deeper, explore the following courses and guided labs:
-
Deploy a Hugo Website with Cloud Build and Firebase Pipeline: Learn how to use Hugo, the popular open source static site generator (written in Go!), with Firebase and Cloud Build.
-
Manage Kubernetes in Google Cloud: Learn all about GKE, including how to build Kubernetes Engine clusters and manage them with
kubectl
.
You can sign up for Google Cloud Innovators at no cost to get 35 credits every month to take these labs and more, so you can stay up to date with the latest skills in cloud and cloud-based applications.
And that’s it! Go continues to make improvements to PGO, so you will see improvements in your application performance with every release. Be sure to create new profiles as your project, load, or runtime conditions change, and keep your default.pgo
committed to source control so you can take full advantage