Skip to content

Skip recompiling dependents when a module interface is unchanged (gfortran)#1289

Open
krystophny wants to merge 1 commit into
fortran-lang:mainfrom
krystophny:fix/interface-aware-rebuild
Open

Skip recompiling dependents when a module interface is unchanged (gfortran)#1289
krystophny wants to merge 1 commit into
fortran-lang:mainfrom
krystophny:fix/interface-aware-rebuild

Conversation

@krystophny

@krystophny krystophny commented May 28, 2026

Copy link
Copy Markdown
Contributor

What

fpm recompiles every object that uses a module whenever that module's source
changes, even when only the implementation changed (a procedure body or a
comment). For a low-level module that rebuilds the whole dependent tree. This
skips dependents whose module interfaces are unchanged.

Impact

In fortplot (290 sources), a one-line edit to a core module recompiled 214 of 289
objects before this change; now it recompiles 1. Editing a procedure body no
longer cascades; editing its interface still does.

This matches CMake on gfortran: the same rebuild set as both the Ninja and the
Unix Makefiles generators on the same fixture. fpm previously rebuilt the full
transitive closure on any change, like a naive b.o: a.o Makefile.

How

  • gfortran leaves a module's .mod byte-identical on an implementation-only edit
    and rewrites it on an interface change, so the .mod is a reliable interface
    signal.
  • After a successful object compile, cache a hash of the .mod files it provides
    and of its dependencies' .mod hashes, next to the existing .digest.
  • In build_target, skip an object whose source and whose dependencies' module
    interfaces are all unchanged, even if a dependency was rebuilt. The scheduler is
    untouched: cascade victims stay queued and are pruned at build time, the shape
    of ninja's restat.
  • Gated on gfortran; every other compiler keeps the current cascade. ifx, flang,
    and nvfortran can follow.

Addresses #967.

Verification

Two-module package; edit only a procedure body in the used module, then rebuild:

before:  gfortran -c m_impl.f90   m_user.f90   main.f90   (whole tree)
after:   gfortran -c m_impl.f90                            (edited module only)

Editing the interface instead correctly recompiles m_impl, m_user, main.

New unit test covers prune / interface-changed / source-changed / missing-record:

$ fpm test fpm-test -- fpm_test_backend
 ... interface-aware-prune [PASSED]

Full unit suite: 187 passed, 0 failed.

Merge order

This PR is independent of most PRs in the parallel-build series but has a
textual overlap with #1298 (mkdir fork gap): both edit the ! critical
block at build_target in fpm_backend.F90. This PR keeps the unnamed
critical; #1298 renames it to critical(run_command). Whichever merges
second needs a rebase on the other.

Independent PRs (any order):

Stacked chain (merge in order):

  1. Fortran compiles via argv (feat: exec Fortran compiles via argv (posix_spawn/CreateProcess) #1299)
  2. Parallel link/archive via argv (feat: run link and archive in parallel via argv spawn #1300)
  3. C/C++ compiles via argv (feat: exec C/C++ compiles via argv, close the shell-fork gap #1301)

Fork-safety fix:

After the above:

When a dependency is recompiled but its gfortran .mod interface is
byte-identical, dependents need not recompile. build_target caches each
object's module-interface fingerprint and the fingerprints of its
dependencies, then prunes a cascade victim whose source and dependency
interfaces are all unchanged. Limited to gfortran, whose .mod files are
a stable interface signal; other compilers keep the conservative cascade.
@krystophny
krystophny force-pushed the fix/interface-aware-rebuild branch from 4d60a2e to 1e0b2b6 Compare May 29, 2026 08:11
@krystophny

Copy link
Copy Markdown
Contributor Author

Benchmark: incremental rebuild on real projects

Measured the edit-rebuild loop on two Fortran projects: fpm itself and fortran-lang/stdlib (stdlib-fpm branch, 182 modules). Two fpm binaries built with identical flags, this branch versus upstream/main. Each project was built clean once, then one source edit per scenario. I report objects actually recompiled (counted by .o mtime, so pruned-but-queued targets do not inflate the number) and wall-clock of fpm build.

Compiler gfortran 12, default (debug) profile, single run, so wall-clock carries some noise; the recompile counts are exact.

fpm (208 objects in the full build)

edit upstream: files / time this branch: files / time
no-op rebuild 0 / 0.14s 0 / 0.14s
leaf file (app/main.f90) 1 / 0.27s 1 / 0.27s
module body, interface unchanged (fpm_strings) 57 / 21.57s 1 / 0.49s
module interface changed (fpm_strings) 61 / 21.70s 55 / 22.35s

stdlib (182 modules)

edit upstream: files / time this branch: files / time
no-op rebuild 0 / 0.97s 0 / 0.98s
small module (stdlib_ansi) 7 / 11.40s 1 / 10.54s
module body, interface unchanged (stdlib_optval) 439 / 20.89s 1 / 10.53s
module interface changed (stdlib_optval) 439 / 20.19s 374 / 19.00s

Reading the numbers

The win is the third row: a body-only change to a widely-used module. The .mod interface is byte-identical, so dependents no longer cascade. On fpm, 57 recompiles drop to 1 (21.6s to 0.5s). On stdlib, 439 drop to 1; the 10.5s that remains is the one slow stdlib_optval compile plus fpm's parse-and-graph front-end, not the cascade.

No-op and leaf edits are unchanged, as expected. When the interface really changes (last row), dependents rebuild as before, with no measurable overhead; the branch even trims the cascade slightly (374 vs 439, 55 vs 61) because second-order dependents whose own interface stayed stable are pruned too.

Limited to gfortran, whose .mod files are reproducible across recompiles. Other compilers keep the existing conservative cascade.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant