Skip to content

Add heuristic to order harness codegen - #4257

Merged
feliperodri merged 8 commits into
model-checking:mainfrom
AlexanderPortland:codegen-ordering
Aug 16, 2026
Merged

feliperodri merged 8 commits into
model-checking:mainfrom
AlexanderPortland:codegen-ordering

Conversation

@AlexanderPortland

@AlexanderPortland AlexanderPortland commented Jul 31, 2025 •

Copy link
Copy Markdown
Contributor

Motivation

When compiling with more than a single thread (as in #4236), the order in which we codegen harnesses can have a non-negligable impact on performance. Specifically, if we handle harness that will generate a lot of code near the end of compilation, the main compiler thread can get stuck waiting for worker threads to export that code, slowing down overall compilation. If we could ensure that more difficult to export harnesses are handled first, the thread pool will have more time to write them to disk off the critical path of compilation.

Approach

This PR switches reachability analysis for all harnesses to happen before codegen. It can then use the information we gather from reachability to reorder codegen based on the total number of items reachable from each harness (MostReachableItems).

This heuristic was chosen because it is:

  • simple -- only requiring a single field access for each harness, and taking only ~10µs to fully sort a typical CodegenUnit of 66 harnesses (acceptable given it took ~20ms to codegen the fastest harnesses in that same CodegenUnit)
  • seemingly effective -- on the s2n-codec benchmark it only ever places a slower-to-export harness after a faster one if their actual export times were within 10ms (or 7%). Since the goal is just to roughly order harnesses based on order of magnitude, this is a very good result 🎉

Results

This change made a small, but not insignificant 2.9% reduction in the end to end compile time of the standard library (@ commit 177d0fd).

And below is a recreation of the graph of the compiler's per-thread CPU usage from #4248 now that codegen is ordered.
Screenshot 2025-07-31 at 12 16 24 PM

While functionally complete on its own, this change will not really make a performance impact until #4236 is merged.

Resolves #4248

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@AlexanderPortland
AlexanderPortland requested a review from a team as a code owner July 31, 2025 19:08
@github-actions github-actions Bot added Z-EndToEndBenchCI Tag a PR to run benchmark CI Z-CompilerBenchCI Tag a PR to run benchmark CI labels Jul 31, 2025
@tautschnig

Copy link
Copy Markdown
Member

Let's get #4236 merged first so that this PR can get concrete data points in its PR description.

@tautschnig

Copy link
Copy Markdown
Member

#4236 has now been merged, so this one should be ready to be acted upon.

Comment thread kani-compiler/src/kani_middle/codegen_order.rs Outdated

@zhassan-aws zhassan-aws left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Is it possible to add a test that ensures the order is what we expect it to be?

github-merge-queue Bot pushed a commit that referenced this pull request Aug 6, 2025
Kani currently has a special `handle_quantifiers()` pass after codegen
that we need to recursively inline function calls inside quantifier
expressions as needed by our hooks. This has to be done for every value
in the symbol table, but our current implementation implicitly clones
**all `Stmt` type values**, even if they don't contain any quantifiers
and wouldn't need to change. This makes the pass currently take up **~7%
of codegen on crates that don't even use quantifiers**.

Now, the `handle_quantifiers_in_stmt()` function just returns an option
with a new `Stmt` if inlining was needed, or `None` otherwise, allowing
us to avoid any allocations and even needing to insert back into the
table in the common case where a `Stmt` doesn't contain any quantifiers.

### Results
Flamegraphing shows that this change reduces the `handle_quantifiers()`
pass from 7.1% to just 0.8% of codegen for the `s2n-codec` crate.

There was a much less significant change on larger codebases like the
standard library, which only saw **a <1% improvement in end to end
compile time** (at std commit
[177d0fd](model-checking/verify-rust-std@177d0fd),
assuming #4257 & #4259 are merged into Kani). That being said, there was
a significant **1-7% decrease in codegen time** for individual crates of
the standard library, so it seems to still make a difference, even if
not reflected in e2e time.

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.
github-merge-queue Bot pushed a commit that referenced this pull request Aug 6, 2025
Internally, Kani uses the `call_with_panic_debug_info()` function to
call closures with a string that explains what they're trying to do,
allowing better error messages if they internally panics. However, this
function is currently called for every harness we codegen and has to
pretty format the harness' function instance--something that calls into
rustc internals and takes a non-negligible amount of time.

We should instead accept a closure that can generate debug info, but
only when (and if) it's needed.

### Results
Based on initial flamegraphing, the formatting passed to
`call_with_panic_debug_info()` went from taking 2.3% of codegen on
`s2n-codec` to not being called a single time (since there are no
compiler errors in a typical execution).

This change made a solid **4.6% reduction in the end to end compile time
of the standard library** (at std commit
[177d0fd](model-checking/verify-rust-std@177d0fd);
assuming #4257, #4259 & #4268 are already merged into Kani).

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.
github-merge-queue Bot pushed a commit that referenced this pull request Aug 6, 2025
Currently, we have to create a new `BodyTransformer` for each harness
individually. However, this takes a lot of time (as we have to
constantly re-validate all our Kani functions and reinitialize
everything based on the `QueryDb`) and the transformer's options are all
shared between harnesses of the same codegen unit anyway.

This PR makes the `BodyTransformer` struct `Clone`-able, allowing us to
initialize a single 'template' transformer for each codegen unit and
then cheaply clone the template for each harness within the unit. Based
on testing, the clone takes just ~3µs when using the default # of
transformation passes, which is much faster than initialization.

### Results
This change made a noticeable **4.8% reduction in the end to end compile
time of the standard library** (at std commit
[177d0fd](model-checking/verify-rust-std@177d0fd)
& assuming #4257 is merged into Kani).

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to improve kani-compiler end-to-end compile performance when exporting goto binaries in parallel by ordering harness codegen so “larger” harnesses (by reachable-item count) are handled earlier, keeping export work off the main thread’s critical path.

Changes:

  • Introduces ReachabilityInfo to package reachability results (starting items, reachable items, call graph) for reuse across phases.
  • Adds a MostReachableItems heuristic and iterator adaptor to reorder harnesses within codegen units based on reachable item count.
  • Refactors the cprover gotoc backend to run reachability before harness codegen and plumb reachability data through codegen_items.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
kani-compiler/src/kani_middle/reachability.rs Adds ReachabilityInfo wrapper to reuse reachability results.
kani-compiler/src/kani_middle/mod.rs Exposes new codegen_order module.
kani-compiler/src/kani_middle/codegen_order.rs Implements heuristic-based harness ordering utilities.
kani-compiler/src/codegen_cprover_gotoc/mod.rs Re-exports HarnessWithReachable for use by ordering code.
kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs Refactors harness codegen pipeline to precompute reachability and (intended) ordering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs Outdated
Comment thread kani-compiler/src/kani_middle/codegen_order.rs
Comment thread kani-compiler/src/kani_middle/codegen_order.rs Outdated
Comment thread kani-compiler/src/kani_middle/codegen_order.rs Outdated
Comment thread kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs Outdated
Comment thread kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs Outdated
@feliperodri feliperodri self-assigned this Aug 14, 2026
feliperodri and others added 2 commits August 16, 2026 18:40
Rework the codegen-ordering heuristic so it no longer regresses peak memory.

The previous approach ran reachability for every harness up front and *retained*
each harness's full result (reachable set, call graph, and its own
BodyTransformation) so it could sort before codegen. On the standard library
this tripled peak memory (~2GB -> ~6GB), because a codegen unit's harnesses were
all held in memory at once.

Instead, order harnesses *within* each codegen unit (preserving main's single
shared-transformer-per-unit model) and retain only the heuristic rating - a
single usize per harness - not the reachable set or call graph. The reachability
run to compute the rating reuses the unit's shared transformer, warming its body
cache, so codegen (which recomputes reachability) hits that cache and only the
graph traversal is repeated, not body transformation. Peak memory therefore
matches codegening the unit in its original order. `codegen_items` is now
byte-for-byte identical to the pre-PR version, so per-harness codegen semantics
are unchanged; only the order within a unit differs.

Also:
- extract the ordering into `kani_middle::codegen_order` with a `CodegenHeuristic`
  trait, the `MostReachableItems` heuristic, and `order_harnesses`;
- add unit tests pinning the sort order (addresses the reviewer request for an
  ordering test);
- fix module-doc grammar and the truncated `MostReachableItems` doc;
- the undefined `ordered_harnesses`/`CallGraph: Clone`/unused-`instances` build
  errors flagged in review no longer apply under this design.

Co-authored-by: Alexander Portland <alexander.portland@users.noreply.github.com>
Signed-off-by: Felipe Monteiro <felisous@amazon.com>
@feliperodri

Copy link
Copy Markdown
Member

Pushed a new revision that gets this PR back to a compiling, reviewable state and resolves the peak-memory regression discussed in the earlier comments.

The branch no longer compiled. The last merge from main left codegen_crate half-refactored (ordered_harnesses was undefined while a leftover per-unit loop still referenced an out-of-scope unit). This was the root cause behind the failing CI jobs (Kani CI, Check Std Verification, Release Bundle, Kani Extra, and the format check). It now builds cleanly on the current toolchain.

Bounded-memory rework

The previous approach ran reachability for every harness up front and retained each harness's full result (reachable set, call graph, and its own BodyTransformation) so it could sort before codegen. On the standard library this tripled peak memory (~2 GB → ~6 GB), because all of a codegen unit's harnesses were held in memory at once.

The new approach:

  • Orders harnesses within each codegen unit, preserving main's single shared-transformer-per-unit model.
  • Retains only the heuristic rating — a single usize per harness — not the reachable set or call graph.
  • Runs the rating pass with the unit's shared transformer, which warms its body cache; codegen then recomputes reachability but hits that cache, so only the (cheap) graph traversal is repeated, not body transformation.

Net effect: peak memory matches codegening the unit in its original order, and codegen_items is now byte-for-byte identical to the pre-PR version — per-harness codegen semantics are unchanged, only the order within a unit differs.

The ordering logic now lives in kani_middle::codegen_order: a CodegenHeuristic trait, the MostReachableItems heuristic, and an order_harnesses helper.

Scope note

MostReachableItems orders harnesses within each codegen unit; units themselves are still processed in their original order. This matches the original intent, but flagging it in case global ordering was expected.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (1)

kani-compiler/src/codegen_cprover_gotoc/compiler_interface.rs:413

  • This runs an extra full reachability traversal for every multi-harness unit even when no export worker exists. thread_pool_size returns 0 on single-core builds, and ThreadPool::send_work then exports synchronously (file_writing_pool.rs:58-64), so ordering cannot hide any export latency and only increases compilation time. Gate the heuristic on the worker count.
                        let ordered_harnesses = order_harnesses::<MostReachableItems>(
                            &unit.harnesses,
                            tcx,
                            &mut shared_unit_transformer,
                        );

`thread_pool_size` returns 0 on single-core builds, and `ThreadPool::send_work`
then exports goto files synchronously on the main thread. With no worker threads
to overlap with, reordering codegen cannot hide any export latency, so running
the extra reachability traversal to rate harnesses would only add compile time.

Gate the ordering on `ThreadPool::has_workers()`, falling back to the harnesses'
original order otherwise.

Signed-off-by: Felipe Monteiro <felisous@amazon.com>
@feliperodri
feliperodri enabled auto-merge August 16, 2026 19:42
@feliperodri
feliperodri added this pull request to the merge queue Aug 16, 2026
Merged via the queue into model-checking:main with commit ba45d79 Aug 16, 2026
41 of 50 checks passed
feliperodri added a commit to AlexanderPortland/kani that referenced this pull request Aug 16, 2026
Three conflicts, all from main growing code the cache touches:

- `current_fn.rs`: both sides added imports. Keep both.

- `compiler_interface.rs`: main added harness reordering (model-checking#4257). The cache
  clear belongs inside the loop body, so it now runs per harness over main's
  `ordered_harnesses` instead of the old `unit.harnesses`.

- `span.rs`: main added a per-call-site `extra_pragmas` parameter (model-checking#4715) that
  the cache has to account for, since a cached `Location`'s pragmas are now
  function-level *plus* call-site-level. `pragmas_for` combines the two, and
  allocates only when both are non-empty: the function's own pragmas come from
  `CurrentFnCtx` and are reused as-is when there are no extras, so the common
  paths leak nothing per location. Also adopted main's corrected
  "nonexistent check" panic wording.

Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-CompilerBenchCI Tag a PR to run benchmark CI Z-EndToEndBenchCI Tag a PR to run benchmark CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add heuristic for ordering harness codegen

5 participants