Profile-Guided Optimization (PGO)

Ship a release binary tuned to how your program actually runs — one command, no manual LLVM steps. Nyra instruments, trains, merges profiles, and rebuilds for you.

What is PGO?

A normal --release build optimizes using static guesses: which branches are likely, which functions are hot, how big a loop unroll should be. Profile-Guided Optimization records real execution counts from a training run, then feeds that data back into LLVM so the final binary matches your workload.

You get JIT-like intelligence without a JIT: no extra runtime, no warmup tax, no background compiler thread — just a faster native executable.

Job: production-speed binary in one step

Job: Build the fastest native binary for this machine when CPU-bound loops, math, or branchy logic dominate runtime.

nyra build --pgo .

--pgo implies --release (LLVM opt -O3, thin LTO, native link). Output lands at target/release/main (or your -o name). Use nyra run for dev; use nyra build --pgo when you ship or benchmark.

Five-phase pipeline

Nyra prints progress as PGO: phase N/5:

PhaseWhat happens
1 — Instrumentllvm opt runs pgo-instr-gen on Nyra’s .ll (Clang does not instrument pre-generated IR by itself), then links a temp binary with -fprofile-instr-generate.
2 — TrainRun main plus every discovered test fn / test_* harness. LLVM writes .profraw files.
3 — Mergellvm-profdata mergetarget/release/pgo/nyra.profdata.
4 — Optimizellvm opt -O3,pgo-instr-use on the IR, then link with -fprofile-instr-use + thin LTO.
5 — CacheStore a fingerprint; unchanged sources skip phases 1–3 on the next build.
sources → instrumented_bin → training (.profraw) → nyra.profdata → optimized release binary

Benefits (why bother?)

When to use it

Good fitSkip PGO
CLI tools, game logic, parsers, benchmarksTiny scripts where build time > run time
Servers with stable hot paths (train with representative requests)I/O-bound apps (network/disk bound)
Libraries you ship as a standalone binary on the hostCross-compiled or wasm builds
Final release / CI perf gates--cdylib shared libraries

Prerequisites

Release and PGO builds need the full LLVM toolchain — Apple Clang alone is not enough. Nyra looks for opt and llvm-profdata on PATH, via brew --prefix llvm, or under /opt/homebrew/opt/llvm.

PlatformInstall
macOS (Homebrew)brew install llvm then export PATH="$(brew --prefix llvm)/bin:$PATH"
Fedoradnf install llvm
Debian/Ubuntuapt install llvm

Disk space: the Homebrew LLVM bottle needs ~2 GB installed plus temporary space during pour. Keep at least 5 GB free on the volume that holds /opt/homebrew (or your Homebrew prefix).

CLI flags

FlagJob
--pgoFull automated pipeline (instrument → train → merge → optimized rebuild). Host native binaries only.
--pgo-arg ARGExtra argv for instrumented runs (repeatable). Example: benchmark size or config path.
--pgo-timeout SECSPer-training-run timeout (default 120). Increase for heavy workloads.
--pgo-generateLow-level Clang instrumentation only (manual workflow).
--pgo-use FILELow-level: rebuild using an existing .profdata file.

nyra run --pgo is rejected — build first, then execute target/release/main.

Training workloads

Nyra profiles what actually executes during training. Make that representative of production:

  1. main() — always run on the instrumented binary.
  2. Tests — every test fn and fn test_* in the project tree (same discovery as nyra test) gets an instrumented harness and a training run.
  3. Comparison benchmarks — when building under examples/comparison/, Nyra also trains examples/comparison/pgo_training/*.ny (branch/mod workloads) for richer branch profiles.
  4. nyra.mod — add pgo-run lines for fixed training argv:
# nyra.mod
module bench
pgo-run --quick
pgo-run --iterations 1000000

CLI --pgo-arg values are appended after manifest args. Inside training binaries, NYRA_PGO=1 is set so your code can branch into a benchmark mode.

Training must exit cleanly (return from main or libc exit) so LLVM flushes .profraw. Abrupt _exit without flush loses profile data.

Tight numeric loops (e.g. cpu_bound): gains are often modest (0–5%) when the release build is already LLVM-optimized; verify with llvm-profdata showmain should show ~loop-iteration counts, not only runtime helpers. If PGO matches release, that is normal for a single hot loop with no branches.

Smart cache

Artifacts live under target/release/pgo/:

If you change only comments, Nyra may still rebuild; if sources and flags match the fingerprint, you see PGO: cache hit and skip instrument/train/merge — only the final link runs if needed.

Examples

# Project release build with PGO
nyra build --pgo .

# Single-file CPU benchmark
nyra build --pgo examples/comparison/cpu_bound/bench.ny

# Train with custom workload args
nyra build --pgo . --pgo-arg --benchmark --pgo-arg --size 10000000

# Long-running training
nyra build --pgo . --pgo-timeout 600

Compare against plain release: nyra build --release . then ./scripts/bench.sh or your own timer. See examples/comparison/ and Built-in methods.

Limitations

Manual / advanced

Power users can split the Clang steps:

nyra build . --release --pgo-generate -o train_bin
LLVM_PROFILE_FILE=default.profraw ./train_bin
llvm-profdata merge -output=nyra.profdata default.profraw
nyra build . --release --pgo-use nyra.profdata

Most projects should prefer --pgo — it wires the same flags, discovers tests, merges profiles, and caches results.