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:
| Phase | What happens |
|---|---|
| 1 — Instrument | llvm 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 — Train | Run main plus every discovered test fn / test_* harness. LLVM writes .profraw files. |
| 3 — Merge | llvm-profdata merge → target/release/pgo/nyra.profdata. |
| 4 — Optimize | llvm opt -O3,pgo-instr-use on the IR, then link with -fprofile-instr-use + thin LTO. |
| 5 — Cache | Store 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?)
- Real-world speed — CPU-bound code (loops, numeric kernels, parsers) often gains ~10–30% over plain
--releasewhen training matches production paths. - Smarter inlining — LLVM inlines callees that actually matter in your runs, not every small function.
- Better code layout — hot basic blocks cluster for instruction-cache locality.
- Branch prediction hints —
if/matcharms ordered by measured frequency. - Zero runtime cost — unlike JVM or JavaScript engines, there is no profiler thread or deoptimization at runtime.
- Fast iteration — smart cache reuses
nyra.profdatawhen sources and flags are unchanged.
When to use it
| Good fit | Skip PGO |
|---|---|
| CLI tools, game logic, parsers, benchmarks | Tiny 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 host | Cross-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.
| Platform | Install |
|---|---|
| macOS (Homebrew) | brew install llvm then export PATH="$(brew --prefix llvm)/bin:$PATH" |
| Fedora | dnf install llvm |
| Debian/Ubuntu | apt 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
| Flag | Job |
|---|---|
--pgo | Full automated pipeline (instrument → train → merge → optimized rebuild). Host native binaries only. |
--pgo-arg ARG | Extra argv for instrumented runs (repeatable). Example: benchmark size or config path. |
--pgo-timeout SECS | Per-training-run timeout (default 120). Increase for heavy workloads. |
--pgo-generate | Low-level Clang instrumentation only (manual workflow). |
--pgo-use FILE | Low-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:
main()— always run on the instrumented binary.- Tests — every
test fnandfn test_*in the project tree (same discovery asnyra test) gets an instrumented harness and a training run. - Comparison benchmarks — when building under
examples/comparison/, Nyra also trainsexamples/comparison/pgo_training/*.ny(branch/mod workloads) for richer branch profiles. nyra.mod— addpgo-runlines 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 show — main 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/:
nyra.profdata— merged profile used for the optimized buildpgo.fingerprint— hash of sources + link/PGO optionsinstrumented_bin— last instrumented executabledefault_*.profraw— raw counters from training (merged then reused)
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
- Host only — PGO runs the instrumented binary on this machine; not available with
--for/ cross targets. - No wasm — wasm32 targets do not support instrumentation profiling in this pipeline.
- No
--cdylib— PGO is for executables you can execute during training. - Training quality = optimization quality — if training never hits a hot path, PGO cannot optimize it.
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.