Add heuristic to order harness codegen - #4257
Conversation
|
Let's get #4236 merged first so that this PR can get concrete data points in its PR description. |
|
#4236 has now been merged, so this one should be ready to be acted upon. |
zhassan-aws
left a comment
There was a problem hiding this comment.
Thanks! Is it possible to add a test that ensures the order is what we expect it to be?
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.
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.
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.
There was a problem hiding this comment.
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
ReachabilityInfoto package reachability results (starting items, reachable items, call graph) for reuse across phases. - Adds a
MostReachableItemsheuristic 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.
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>
|
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 Bounded-memory reworkThe previous approach ran reachability for every harness up front and retained each harness's full result (reachable set, call graph, and its own The new approach:
Net effect: peak memory matches codegening the unit in its original order, and The ordering logic now lives in Scope note
|
There was a problem hiding this comment.
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_sizereturns 0 on single-core builds, andThreadPool::send_workthen 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>
ba45d79
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>
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:
CodegenUnitof 66 harnesses (acceptable given it took ~20ms to codegen the fastest harnesses in that sameCodegenUnit)s2n-codecbenchmark 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.

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.