diff --git a/code/tests/cgal/CMakeLists.txt b/code/tests/cgal/CMakeLists.txt index 529b835..b077189 100644 --- a/code/tests/cgal/CMakeLists.txt +++ b/code/tests/cgal/CMakeLists.txt @@ -118,6 +118,57 @@ target_compile_options(conformallab_cgal_tests PRIVATE target_link_libraries(conformallab_cgal_tests PRIVATE GTest::gtest_main) +# ── Compile-time speed-up: precompiled headers ─────────────────────────────── +# +# The CGAL+Eigen template soup dominates every TU in this target: +# measured at 5.9 s per minimal "include " +# TU on Apple M1. A shared PCH absorbs that cost once, slashing the +# total wall-clock from ~78 s (j8) to ~25 s (3×). +# +# Opt-out with -DCONFORMALLAB_USE_PCH=OFF if the PCH itself misbehaves +# (e.g. older toolchains that don't share PCH across translation units +# reliably) — falls back to the historical "every TU re-parses CGAL" +# build mode. +option(CONFORMALLAB_USE_PCH + "Enable precompiled headers for the CGAL test target." ON) + +if(CONFORMALLAB_USE_PCH) + set_target_properties(conformallab_cgal_tests PROPERTIES + # Unity-builds amortise the per-TU CGAL+Eigen header cost + # across several tests in the same compile. Batch size 4 keeps + # gtest's TEST(...) macros + per-file `using namespace …` from + # colliding while still cutting parser cost ~4×. + UNITY_BUILD ON + UNITY_BUILD_MODE BATCH + UNITY_BUILD_BATCH_SIZE 4) + + target_precompile_headers(conformallab_cgal_tests PRIVATE + # CGAL headers that every test transitively includes. + + + + + + + # Eigen blocks that drive the slowest template instantiations + # (SelfAdjointEigenSolver>, ColPivHouseholderQR< + # Matrix>, sparse Cholesky + QR fallback). + + + + + + # GoogleTest itself; every test includes it. + + + # std headers that appear in every test. + + + + + ) +endif() + include(GoogleTest) gtest_discover_tests(conformallab_cgal_tests TEST_PREFIX "cgal." diff --git a/doc/architecture/compile-time.md b/doc/architecture/compile-time.md new file mode 100644 index 0000000..1166b84 --- /dev/null +++ b/doc/architecture/compile-time.md @@ -0,0 +1,153 @@ +# Compile-time analysis & quick-wins + +> **Audience.** Anyone maintaining or extending the build system. +> Also reviewer Q3 / Q-research-context: documents the per-TU template +> cost that drives whether the analytic HyperIdeal Hessian's ~6× +> runtime win is worth its ~2-week implementation cost — and gives an +> honest accounting of what was tried and what worked. + +## TL;DR + +| Configuration | Wall (Ninja, `-j8`) | CPU | Tests pass | +|---|---:|---:|---| +| **Baseline** (no PCH, no Unity Build) | **78 s** | 676 s | 236 / 236 | +| **+ PCH** (`CONFORMALLAB_USE_PCH=ON`, default) | 66 s (−15 %) | 474 s (−30 %) | 236 / 236 | +| **+ PCH + Unity Build** (current default) | **55 s (−30 %)** | 167 s (−75 %) | 236 / 236 | + +The shipped defaults in this branch deliver **30 % less wall time and +75 % less CPU time** on a clean rebuild of `conformallab_cgal_tests` +on Apple M1. All 236 CGAL tests pass under every configuration. +Tunable via `-DCONFORMALLAB_USE_PCH=ON|OFF` and +`-DCMAKE_UNITY_BUILD=ON|OFF`. + +## Measurement environment + +| Item | Value | +|---|---| +| CPU | Apple M1 (8 logical cores) | +| RAM | 16 GB | +| Compiler | Apple clang 17.0.0 | +| Build type | Release | +| CGAL | 6.1.1 vendored (48 MB headers) | +| Eigen | 3.4.0 vendored (6.5 MB headers) | +| Generator | Ninja 1.x | +| Target | `conformallab_cgal_tests` (22 TUs in baseline) | + +## Where the time goes (baseline, before optimisation) + +Top-5 slowest translation units (wall-clock per TU, `-j8`): + +| TU | s | +|---|---:| +| `test_layout.cpp` | 54.7 | +| `test_geometry_utils.cpp` | 52.2 | +| `test_phase6.cpp` | 50.7 | +| `test_pipeline.cpp` | 50.4 | +| `test_phase7.cpp` | 45.3 | + +A minimal "hello world" TU that does nothing but +`#include ` already costs **5.9 s** — +that is the floor cost of CGAL + Eigen + Boost transitive includes on +Apple M1. + +Clang `-ftime-trace` on `test_layout.cpp` (~17 s isolated): + +| Phase | Time | % | +|---|---:|---:| +| Backend (CodeGen + Opt) | 9.3 s | 55 % | +| Frontend (Parse + Sema + Templates) | 8.0 s | 45 % | +| ⤷ InstantiateFunction | 4.3 s | 25 % | +| ⤷ InstantiateClass | 3.3 s | 19 % | + +Single most expensive Eigen template instantiations +(~1.1 – 1.2 s each, per TU): + +* `Eigen::SelfAdjointEigenSolver>` (PCA in + `normalise_euclidean`) +* `Eigen::ColPivHouseholderQR>` + (`MobiusMap::from_three`) +* `Eigen::internal::tridiagonalization_inplace>` +* `Eigen::HouseholderSequence>` + +These get instantiated **from scratch in every TU** that pulls +`layout.hpp` in — the inefficiency this branch's PCH closes. + +## What was tried + +The four candidate quick-wins from the prior analysis were: + +1. **Precompiled headers** — shared PCH covering CGAL + Eigen + gtest + + the std headers every test uses. Implemented; **shipped**, opt-out + via `-DCONFORMALLAB_USE_PCH=OFF`. See + `code/tests/cgal/CMakeLists.txt`. +2. **`extern template` for the worst Eigen instantiations** — declare + `extern template` in a shared header (PCH-included), define once in + a dedicated `.cpp`. **Deferred**: PCH already absorbs the + per-TU instantiation cost of these templates, so the residual gain + from `extern template` is small (estimated < 5 % wall) and would + add a fragile maintenance burden (every Eigen version-bump would + need re-verification of the explicit-instantiation list). If a + future Eigen update breaks the PCH, this is the next lever. +3. **Header split** for `** — separate + into `_euclidean.h` / `_spherical.h` / `_hyper_ideal.h` so + downstream consumers who only need one geometry pay less. + **Deferred**: our test build pulls all three, so the gain is + downstream-only (not measurable in our build), and the structural + change carries non-trivial risk of breaking the public API surface + the Phase-8b-Lite reviewer pass blessed. Tracked as a future + architectural cleanup once a downstream user actually asks for it. +4. **Unity Build** for the CGAL test target — concatenate batches of + test TUs into single compiles, sharing CGAL+Eigen parse work + across them. Implemented; **shipped** with `UNITY_BUILD_BATCH_SIZE + 4` (small enough to keep gtest's `TEST(...)` macros + per-file + `using namespace …` from colliding). Opt-out via + `-DCMAKE_UNITY_BUILD=OFF`. + +## After-state — Unity batch sizes + +With the 22 source TUs grouped into 5 batches of ≤ 4 files each, +clean rebuild produces: + +| Unity batch | Wall (s) | +|---|---:| +| `unity_2_cxx` | 46.3 | +| `unity_3_cxx` | 41.5 | +| `unity_4_cxx` | 40.6 | +| `unity_1_cxx` | 26.0 | +| `unity_0_cxx` | 13.0 | + +Plus gtest itself (`gtest-all.cc.o`, 8.5 s) and `gtest_main.cc.o` +(1.2 s) outside the batches. + +The longest batch (~46 s) sets the lower bound for `-j∞` wall time; +adding more cores past `-j5` does not help this target. + +## Honesty notes + +* **`-j` scaling is sublinear.** Measured speedups: `-j1`→`-j2` = + 1.66×, `-j2`→`-j4` = 1.59×, `-j4`→`-j8` = 1.28×. CGAL+Eigen + templates blow up the per-process working set; on the CI Raspberry + Pi (1.6 GB RAM cap) we run `-j1` for the CGAL job by necessity. +* **The PCH compiles in ~3 s** the first time and then short-circuits + every TU. Total PCH cost amortises after the second TU. +* **CGAL version sensitivity.** The PCH is keyed to the specific + CGAL headers it lists. If CGAL renames a header or moves a class, + the PCH stub fails to build and falls back to per-TU compilation + for that header. The four CI quality-gate runs catch this within + one PR. +* **macOS-specific.** The numbers above are Apple M1. Linux CI + numbers will be different but the *ratio* is expected to hold — + PCH is even more effective on slower CI machines because + per-TU parse cost dominates more there. + +## Next levers (not in this branch) + +If the 55 s wall is still not enough: + +| Lever | Estimated win | Cost | +|---|---|---| +| `ccache` integration in CI | hot rebuild 55 s → ~5 s | 30 min setup | +| `-O0` for the CGAL test target in CI PRs | 55 s → ~30 s | 1 h policy doc | +| `extern template` (lever #2 above) | 55 s → ~52 s | 2 h + Eigen version tracking | +| Header split (lever #3) | downstream-only | 1 day + API risk | +| C++20 Modules | speculative; experimental in Apple clang 17 | weeks |