- CMake 87%
- C 13%
CMake PGO Abstraction
cmake/pgo.cmake + cmake/pgo_driver.cmake (+ cmake/pgo_external.cmake) add
Profile-Guided Optimization to an existing CMake target, for GCC and Clang.
Want a plain
cmake --buildto just produce an optimized binary, with no driver, stages, or second build -- on both compilers? Consume the library via the external dependency entry point; it hides the GCC two-pass inside the sub-build.
Usage
include(pgo) # cmake/ on CMAKE_MODULE_PATH
add_library(mathlib STATIC ...) # your existing target
add_executable(demo_bench ...) # drivers that exercise it
add_executable(demo_app ...)pgo_train_runs(demo_bench
ARGS "--size 100 --iters 200"
ARGS "--size 10000 --iters 50")pgo_train_runs(demo_app
ARGS "data/small.txt")pgo_optimize(mathlib TRAIN_TARGETS demo_bench demo_app)Building the optimized library
Three ways, all keeping the same pgo_train_runs / pgo_optimize setup above:
-
Driver (one command, fully automated, both compilers):
CC=clang cmake -DSRC=. -DBUILD=build -P cmake/pgo_driver.cmake # or CC=gccAfter it finishes,
mathlibis built with-fprofile-use. -
Convenience targets --
pgo_optimizecreates a per-target<target>_pgotarget and extends a single aggregatepgotarget (which drives every optimized target at once):- Clang:
cmake --build build --target pgo(ormathlib_pgo) builds the optimized library in one pass. A plaincmake --build buildalso produces it -- on Clang the optimization is in-tree, so PGO is effectively the default build. - GCC: configure with
-DPGO_STAGE=generate, thencmake --build build --target pgobuilds the instrumented library, runs training, and prints the exact reconfigure + build commands to produce the optimized library (-DPGO_STAGE=use). A plaincmake --buildcannot be the PGO default on GCC -- the two-pass needs the reconfigure between builds. For a seamless default build on GCC, use the external dependency (option 3).
- Clang:
-
As an external dependency -- a seamless single build for both compilers; see "External dependency" below.
Requiring PGO
If a library is only correct when profile-guided, add REQUIRED so a non-PGO build
fails loudly instead of silently producing an unoptimized binary:
pgo_optimize(mylib TRAIN_TARGETS trainer REQUIRED)- GCC, default (off) stage:
cmake -S . -B buildaborts at configure time and prints the exact PGO build commands (the staged commands and the one-shot driver). The generate stage builds but warns that it is the instrumented, not-final library; the use stage is accepted. - Clang: a plain
cmake --buildalready applies PGO in-tree, soREQUIREDis a no-op -- there are no separate steps to enforce. - Consumers using
pgo_optimize_external()satisfy the requirement automatically (the driver configuresgeneratethenuse). REQUIREDandGENERATE_ONLYare mutually exclusive.
API
pgo_train_runs(<exe> ARGS "<args>" [ARGS ...])- one run perARGS; repeated calls accumulate; all runs feed one profile.pgo_optimize(<target> TRAIN_TARGETS <exe>... [PROFILE_DIR <dir>] [GENERATE_ONLY] [REQUIRED])- also defines a per-target<target>_pgoand an aggregatepgobuild target (see Building, option 2).REQUIREDfails a non-PGO configuration (see "Requiring PGO").pgo_optimize_external(<name> SOURCE_DIR <proj> LIBRARY <lib> [INCLUDE_DIRS ...] [TOOLCHAIN <file>] [GENERATOR <gen>] [CMAKE_ARGS ...])- build a PGO-enabled sub-project externally and consume its library as anIMPORTEDtarget.
How it works
- Clang builds an instrumented twin of the target + drivers, runs the training,
llvm-profdata merges the result, and applies-fprofile-useto the target in the same build (profiles match by function hash). - GCC keys
.gcdaby object path, so a twin cannot match. The driver does a same-build-directory two-pass:generate -> build -> train -> use -> rebuild, reusing identical object paths so the profile matches.
External dependency
cmake/pgo_external.cmake builds a PGO-enabled sub-project as an ExternalProject
and exposes its library as an IMPORTED target. The whole PGO process (the GCC
two-pass included) runs inside the sub-build during a normal cmake --build, so the
consumer never deals with stages or the driver:
include(pgo_external)pgo_optimize_external(mathlib_opt
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib # a self-contained, PGO-enabled project
LIBRARY mathlib # static lib target it builds
INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/lib/include)add_executable(consumer consumer.c)target_link_libraries(consumer PRIVATE mathlib_opt)CC=clang cmake -G Ninja -S . -B build # or -DCMAKE_C_COMPILER=gcc
cmake --build build # sub-build runs PGO; consumer links the result
The SOURCE_DIR is an ordinary CMake project that must:
- call
pgo_optimize(<LIBRARY> TRAIN_TARGETS ...)(the sub-build requires it) and register training runs withpgo_train_runs; - install the static library and its headers:
install(TARGETS <LIBRARY> ARCHIVE DESTINATION lib)--DESTINATIONmust be exactlylib, since the import path is<install>/lib/lib<LIBRARY>.a; - build optimized (default
Release/-O2).
The consumer's compiler, build type, and TOOLCHAIN / CMAKE_ARGS are forwarded into
the sub-build so the ABI matches (use TOOLCHAIN for cross builds, e.g. aarch64). Note
that training runs the instrumented binary on the build host, so the sub-build must be
native -- your native aarch64 is fine; an x86 -> aarch64 cross would need an emulator.
Two ready examples: the self-contained external/ demo, and this repo's own top-level
project (its mathlib carries install rules) -- point SOURCE_DIR at the repo root
with LIBRARY mathlib.
Requirements
- CMake >= 3.22 and a single-config generator. The driver uses CMake's default
generator (e.g. Unix Makefiles, or Ninja) unless you pass
-DGEN=<generator>; any single-config generator works (multi-config generators are out of scope). - Clang path needs
llvm-profdataon PATH. - Native build+run (instrumented binary runs on the build host). Cross/on-device collection is out of scope.
- Build at a real optimization level (the demo defaults to
Release/-O2); PGO is a no-op at-O0.