skip to content

Where do you put a Go PGO profile so that `go build` uses it without extra flags?

level: juniorimportance: should knowfreq 24%

answer

  1. the compiler wants evidence, not guesses
  2. an ordinary pprof CPU profile
  3. one fixed filename, beside main
  4. auto is already the default flag
  5. default.pgo in the main package directory

basics

~20 s

Save a pprof CPU profile as a file named default.pgo in the main package's directory. The go command defaults to -pgo=auto, so build, run, install and test pick that file up with no flag. Use -pgo=off to opt out.

solid answer

~40 s

Profile-guided optimization in Go is driven by an ordinary pprof CPU profile collected from a real run of the program. You commit that profile as `default.pgo` in the directory of the `main` package you are building — for example `cmd/api/default.pgo`. Since Go 1.21 the build flag `-pgo=auto` is the default, so `go build ./cmd/api` finds that file and compiles the whole build with it, including dependencies. `-pgo=off` builds without it, and `-pgo=<path>` points at a specific profile instead. There is nothing else to switch on: no instrumented build step, no separate profiling toolchain, and no change to the source. Each `main` package needs its own `default.pgo`; a profile in a library package is ignored.

code

text · 11 lines
text
$ ls ./cmd/api
default.pgo  handlers.go  main.go

# -pgo=auto is the default, so this build uses cmd/api/default.pgo
$ go build -o api ./cmd/api

# baseline build with no profile
$ go build -pgo=off -o api-baseline ./cmd/api

# an explicit profile instead of the committed one
$ go build -pgo=./profiles/peak.pprof -o api-peak ./cmd/api

go deeper

for a junior

Remember the three concrete facts: the input is a pprof CPU profile, the filename is default.pgo, and it lives in the main package's directory. Be able to say that -pgo=auto is already the default and -pgo=off turns it off.

for a middle

Be ready to explain why the file lives beside main rather than at the module root, why each binary needs its own, and that the profile influences the whole build including dependencies, which is why build caches get invalidated.

for a senior

Expect to be asked where the committed profile comes from operationally: which instance, which traffic window, whether several profiles were merged, and how you keep a load-test artifact from being mistaken for a production one.

for a principal

Frame the checked-in profile as a build input like any other dependency: it needs provenance, an owner and a refresh story. Be ready to argue for or against committing a binary artifact into the source repo at all.

## What PGO is in Go Profile-guided optimization (PGO) means feeding a recording of how the program actually behaved back into the compiler, so the compiler optimises the code paths that really run hot rather than guessing from the source alone. Go shipped PGO as a preview in Go 1.20 and made it generally available in Go 1.21. The input is not a special format: it is a normal **pprof CPU profile**, the same protocol-buffer file `runtime/pprof` and the `net/http/pprof` handlers produce, and the same thing `go tool pprof` reads. That matters, because it means the profile can come from a real production instance under real traffic rather than from a synthetic benchmark rig. ## Where the file goes The go command looks for a file literally named `default.pgo` in the **directory of the main package being built**. For a repository laid out as: ``` cmd/api/main.go cmd/api/handlers.go cmd/api/default.pgo ``` `go build ./cmd/api` picks up `cmd/api/default.pgo` automatically. If you build several binaries out of one module, each `main` package directory needs its own profile — there is no module-wide profile, and a `default.pgo` dropped into a library package is simply ignored. ## The -pgo flag `-pgo` is a build flag shared by `go build`, `go install`, `go run` and `go test`, and it takes three shapes: - `-pgo=auto` — the default since Go 1.21: use `default.pgo` from the main package directory if it exists, otherwise build normally. - `-pgo=off` — build with no profile at all, even if `default.pgo` is sitting there. This is how you produce the baseline half of an A/B comparison. - `-pgo=/path/to/some.pprof` — use an explicit profile file. Useful in CI when the profile is fetched from an artifact store instead of committed. Because `auto` is the default, checking the file in is the entire opt-in. Nobody has to remember a flag, which is exactly the point: a developer who clones the repo and runs `go build` gets the same optimisation the release build gets. ## Getting a representative profile The profile should describe the workload you care about. For an HTTP API serving product traffic — a handler that reads an id out of the request path and queries the database — a profile captured from one instance during ordinary daytime traffic is far more useful than one captured from a load test that hammers a single endpoint, because PGO will optimise for whatever the samples say is hot. If you have several instances or several traffic shapes, you can merge profiles before committing them; `go tool pprof` will merge inputs and write a combined proto profile: ``` go tool pprof -proto a.pprof b.pprof c.pprof > cmd/api/default.pgo ``` Merging is usually the right move: an unmerged profile from a single instance can encode that instance's quirks (a stuck client, a cold cache) as the program's hot path. ## What PGO does not require and does not do It does not require a separate instrumented build — Go's sampling profiler runs in a normal binary at low overhead, which is why capturing from production is practical. It does not change program semantics; a PGO build and a non-PGO build of the same source behave identically, they just have different machine code. And it is not a correctness gate: a profile that no longer matches the code is not an error, just a less useful hint (the compiler will simply find fewer matches). The main cost to be aware of is build time and cacheability. The profile influences code generation across the whole build, dependencies included, so introducing or changing `default.pgo` invalidates cached package builds and forces recompilation. That is a build-pipeline consideration rather than a reason to avoid PGO, but it is the reason teams notice the first PGO-enabled CI run taking longer than usual.

  • Your module builds three binaries. Does one committed profile cover all of them?
    No. `-pgo=auto` looks for `default.pgo` in the directory of the `main` package being built, so each binary needs its own profile in its own directory. A profile placed in a shared library package is ignored entirely. If you want one profile for several binaries you have to pass it explicitly with `-pgo=<path>` in each build command.
  • Where does the profile you commit usually come from?
    A CPU profile taken from the program running its real workload — production or a faithful staging replica — because PGO optimises whatever the samples say is hot. Profiles from several instances or time windows are usually merged first, for example with `go tool pprof -proto a.pprof b.pprof > default.pgo`, so that one instance's quirks do not become the program's official hot path.
  • Does building with a profile change what the program does?
    No. PGO only changes code generation — which callees get inlined, which indirect calls get a direct fast path. The observable behaviour of the program is the same as a `-pgo=off` build of the same source, which is why you can compare the two builds under identical traffic and attribute any difference purely to performance.

saying these in an interview costs you the question

  • Thinks PGO needs a separate instrumented build
  • Expects an env var or a go.mod directive instead of a file
  • Puts default.pgo at the module root and expects it to apply
  • Believes -pgo=off disables all compiler optimizations
  • Assumes the profile must come from a benchmark, not real traffic